GraphQL Subscriptions and the Apollo Project provide a type-safe and API forward way to interact with server push and event-based requests such as Web Sockets.
Although the Apollo Project provides many implementations for various infrastructures and technologies, it has unfortunately been unable to implement a universal solution for Subscriptions over AWS API Gateway Web Sockets and Lambda. However, GraphQL’s flexibility allows us to implement our own on top of any infrastructure.
The current implementations for subscriptions in Apollo Server depend on a stateful server, where the subscriptions and publications occur in the same runtime. This is not what happens in the AWS Lambdas where code execution occur in a new runtime every time a Lambda is invoked. Furthermore, since Lambdas are not long-lived, information must be stored regarding which users have connected over a WebSocket and what subscriptions they have requested from the back-end. Even the particular method in which connection and subscription information is stored is not standardized since you can use a few different AWS products to do so such as DynamoDB or AWS Aurora.
Nevertheless, you can still provide a back-end solution that can be used by Apollo Clients without an Apollo Server.
AWS WebSockets are backed by AWS API Gateway and AWS Lambda. AWS exposes routes to handle that can be wired to trigger lambdas to handle specific events. The `$connect` route forwards connection events to a lambda when a client connects to a WebSocket endpoint. The lambda receives a `connectionId` that identifies a client's unique connection and a callback URL you can use to send messages to a client.
In the `$connect` route, you will define a lambda that will store the connection information and the callback URL in a DynamoDB table that you can query later when a message is ready to respond to a client.
Define Your Table
In your table, you want to express a one-to-many relationship between a client and its many possible subscriptions. To do that, define a `clients` table with the following schema.
Your table will define a string hash key called `connectionId` that will always correspond to a client's unique connection ID. You will also define a string range key named `id` that will identify a client record or one of many subscriptions. This pattern is referred to as the adjacency list pattern for which you can read more about here.
Define a `ttl` on all table records so that DynamodDB can clean them up after a connection has expired. When a connection is permitted in the table, you will set the `ttl` attribute to the time of connection plus two hours which is how long a WebSocket connection can last in API Gateway.
Define a Connect Handler
Next, attach a lambda to the `$connect` route.
The lambda will be triggered whenever a client attempts to connect to your WebSocket endpoint. The lambda will then execute the following code.
In this handler, persist the client's `connectionId`, the callback URL you will publish messages to, the time of connection, and a `ttl`. Afterward, return a 200 status code to the client.
You will also attach resolvers to your GraphQL schema that will handle a subscription query.
The resolver will store the subscription method as an adjacency item under the corresponding client along with the request's ID and the same `ttl` the connection was created with.
Handling the GraphQL Subscription Query
You will now define a subscription handler.
The subscription handler is mapped to the `$default` route so that the lambda is triggered on all incoming messages from authorized clients. When this lambda is triggered, you will handle incoming Apollo Client requests.
Any incoming Apollo Client request will have the following format:
The `id` field is a unique ID for the request that can be used to identify a corresponding response. The `type` field is used by Apollo Clients to identify the type of operation that is being requested from the back-end. The `payload` is the data for the query.
You can process this request in the lambda like so:
In this lambda you do the following:
- Verify that you have registered a client's connection in your table
- If it is not an init request, parse the query payload and verify that it is a subscription request. This step can be omitted if the endpoint will handle GraphQL queries other than subscriptions.
- Validate the query against your schema:
- If the request is invalid, you return an error
- If the request is valid GraphQL, execute your query so that your resolvers are executed
- Finally, return a successful response or an error.
After the lambda completes, you should have the subscription persisted in your table and the client should receive a valid subscription response.
Publishing to a Subscription
Define a lambda that publishes new messages to client subscriptions. This lambda is triggered by SNS events form your `messages` topic.
When this lambda is triggered, it will execute the following handler:
This lambda will do the following:
- Query all clients and subscriptions that correspond to the user that the messages are addressed to
- Validate that the `ttl` for those subscriptions has not expired
- For each subscription, construct a payload in the format Apollo Client expects
- Convert the event content into a GraphQL AST value from the Message type and set it as the payload
Publishes the message on the client's callback URL
Connecting a Client
On the client-side, connect to your AWS API Gateway Web Sockets endpoint using a WebSocketClient and Apollo Client.
Now that the client is connected you can request a subscription to your WebSockets endpoint.
When a message is published on the SNS topic, the back-end should publish a new message and the client will show a new message incoming, not the subscription.
In this article, we walked through how to leverage the Apollo and GraphQL libraries to implement GraphQL Subscriptions over Web Sockets on AWS.
GraphQL Subscriptions are a powerful method to model event-based communication between web services. They can provide technology like WebSockets type safety and expressiveness that make APIs safer and easier to use. Furthermore, AWS API Gateway and Lambda provide infrastructure that can make any WebSocket implementation highly scalable and flexible.
This post was originally published on www.yonomi.co.
About the Author
Jorge Martinez Hernandez is a Backend Engineer at Yonomi. A wizard with embedded IoT solutions, he works on core functionality, feature development, and device management for Yonomi ThinCloud, a cloud backend for consumer IoT products. Jorge likes working with well designed, tested and expressive code. Outside of work, he enjoys learning new languages - he is currently learning french - and spending time with his family in Manor, TX.