Uh oh!
There was an error while loading.Please reload this page.
- Notifications
You must be signed in to change notification settings - Fork93
Use AWS Lambda + AWS API Gateway v2 for GraphQL subscriptions over WebSocket and AWS API Gateway v1 for HTTP
License
michalkvasnicak/aws-lambda-graphql
Folders and files
| Name | Name | Last commit message | Last commit date | |
|---|---|---|---|---|
Repository files navigation
📖Documentation foraws-lambda-graphql0.13.0
UseApollo Server Lambda with GraphQL subscriptions over WebSocket (AWS API Gateway v2).
With this library you can do:
- same things as withapollo-server-lambda by utiizing AWS API Gateway v1
- GraphQL subscriptions over WebSocket by utilizing AWS API Gateway v2 andsubscriptions-transport-ws
- Quick start
- Packages
- Infrastructure
- Examples
In this quick example we're going to build a simple broadcasting server that broadcasts messages received usingbroadcastMessage mutation to all subscribed connections.
Skip to final implementation if you don't need a step by step guide
First we need to install dependencies:
yarn add aws-lambda-graphql graphql graphql-subscriptions aws-sdk#ornpm install aws-lambda-graphql graphql graphql-subscriptions aws-sdk
Note thataws-sdk is required only for local development, it's provided by the AWS Lambda by default when you deploy the app
Now we have all the dependencies installed so lets start with server implementation.
Our GraphQL server needs to know how to store connections and subscriptions because Lambdas are stateless. In order to do that we need create instances of theConnection manager andSubscription manager. We have two options of persistent storage for our connections and subscriptions.
DynamoDB:
import{DynamoDBConnectionManager,DynamoDBSubscriptionManager,}from'aws-lambda-graphql';/* By default subscriptions and connections use TTL of 2 hours. This can be changed by `ttl` option in DynamoDBSubscriptionManager and DynamoDBConnectionManager. ttl accepts a number in seconds (default is 7200 seconds) or false to turn it off. It's your responsibility to set up TTL on your connections and subscriptions tables.*/constsubscriptionManager=newDynamoDBSubscriptionManager();constconnectionManager=newDynamoDBConnectionManager({subscriptions:subscriptionManager,});
ttl field in Connections, Subscriptions and SubscriptionOperations tables. You can turn off the TTL by setting upttl option tofalse inDynamoDBSubscriptionManager andDynamoDBConnectionManager.
Redis:
import{RedisConnectionManager,RedisSubscriptionManager,}from'aws-lambda-graphql';importRedisfrom'ioredis';constredisClient=newRedis({port:6379,// Redis porthost:'127.0.0.1',// Redis host});constsubscriptionManager=newRedisSubscriptionManager({ redisClient,});constconnectionManager=newRedisConnectionManager({ subscriptionManager, redisClient,});
In order to be able to broadcast messages (publish events) we need anEvent store. Because our server can received a lot of messages we need to work with events in async, meaning that the actual events are not published directly from mutation but rather they are stored in underlying data store which works as an event source for our server. Because we decided to use DynamoDB as our persistent store, we are goint to use it as our event source.
import{DynamoDBConnectionManager,DynamoDBEventStore,DynamoDBSubscriptionManager,}from'aws-lambda-graphql';/* By default event stores uses TTL of 2 hours on every event. This can be changed by `ttl` option in DynamoDBEventStore. ttl accepts a number in seconds (default is 7200 seconds) or false to turn it off. It's your responsibility to set up TTL on your events table.*/consteventStore=newDynamoDBEventStore();constsubscriptionManager=newDynamoDBSubscriptionManager();constconnectionManager=newDynamoDBConnectionManager({ subscriptionManager,});
That's it for now. OureventStore will use DynamoDB to store messages that we want to broadcast to all subscribed clients.
ttl field in Events store table. This can be turned off by setting up thettl option tofalse.
Our server needs a GraphQL schema. So we'll create one.
import{DynamoDBConnectionManager,DynamoDBEventStore,DynamoDBSubscriptionManager,}from'aws-lambda-graphql';consteventStore=newDynamoDBEventStore();constsubscriptionManager=newDynamoDBSubscriptionManager();constconnectionManager=newDynamoDBConnectionManager({ subscriptionManager,});consttypeDefs=/* GraphQL */` type Mutation { broadcastMessage(message: String!): String! } type Query { """ Dummy query so out server won't fail during instantiation """ dummy: String! } type Subscription { messageBroadcast: String! }`;
From given schema we already see that we need to somehow publish and process broadcasted message. For that purpose we must create aPubSub instance that uses our DynamoDB event store as underlying storage for events.
PubSub is responsible for publishing events and subscribing to events. Anyone can broadcast message usingbroadcastMessage mutation (publish) and anyone connected over WebSocket can subscribed tomessageBroadcast subscription (subscribing) to receive broadcasted messages.
PubSub serializes event payload to JSON. If you don't want this behaviour, setserializeEventPayload option tofalse on yourPubSub instance.
import{DynamoDBConnectionManager,DynamoDBEventStore,DynamoDBSubscriptionManager,PubSub,}from'aws-lambda-graphql';consteventStore=newDynamoDBEventStore();constsubscriptionManager=newDynamoDBSubscriptionManager();constconnectionManager=newDynamoDBConnectionManager({ subscriptionManager,});constpubSub=newPubSub({ eventStore,// optional, if you don't want to store messages to your store as JSON// serializeEventPayload: false,});consttypeDefs=/* GraphQL */` type Mutation { broadcastMessage(message: String!): String! } type Query { """ Dummy query so out server won't fail during instantiation """ dummy: String! } type Subscription { messageBroadcast: String! }`;constresolvers={Mutation:{broadcastMessage:async(root,{ message})=>{awaitpubSub.publish('NEW_MESSAGE',{ message});returnmessage;},},Query:{dummy:()=>'dummy',},Subscription:{messageBroadcast:{// rootValue is same as the event published above ({ message: string })// but our subscription should return just a string, so we're going to use resolve// to extract message from an eventresolve:(rootValue)=>rootValue.message,subscribe:pubSub.subscribe('NEW_MESSAGE'),},},};
Our GraphQL schema is now finished. Now we can instantiate the server so we can actually process HTTP and WebSocket events received by our Lambda server and send messages to subscribed clients.
In order to send messages to subscribed clients we need the last piece and it is aEvent processor. Event processor is responsible for processing events published to our Event store and sending them to all connections that are subscribed for given event.
Because we use DynamoDB as an event store, we are going to useDynamoDBEventProcessor.
import{DynamoDBConnectionManager,DynamoDBEventProcessor,DynamoDBEventStore,DynamoDBSubscriptionManager,PubSub,Server,}from'aws-lambda-graphql';consteventStore=newDynamoDBEventStore();consteventProcessor=newDynamoDBEventProcessor();constsubscriptionManager=newDynamoDBSubscriptionManager();constconnectionManager=newDynamoDBConnectionManager({subscriptions:subscriptionManager,});constpubSub=newPubSub({ eventStore});consttypeDefs=/* GraphQL */` type Mutation { broadcastMessage(message: String!): String! } type Query { """ Dummy query so out server won't fail during instantiation """ dummy: String! } type Subscription { messageBroadcast: String! }`;constresolvers={Mutation:{broadcastMessage:async(root,{ message})=>{awaitpubSub.publish('NEW_MESSAGE',{ message});returnmessage;},},Query:{dummy:()=>'dummy',},Subscription:{messageBroadcast:{// rootValue is same as the event published above ({ message: string })// but our subscription should return just a string, so we're going to use resolve// to extract message from an eventresolve:(rootValue)=>rootValue.message,subscribe:pubSub.subscribe('NEW_MESSAGE'),},},};constserver=newServer({// accepts all the apollo-server-lambda options and adds few extra options// provided by this package connectionManager, eventProcessor, resolvers, subscriptionManager, typeDefs,});exportconsthandleWebSocket=server.createWebSocketHandler();exportconsthandleHTTP=server.createHttpHandler();// this creates dynamodb event handler so we can send messages to subscribed clientsexportconsthandleEvents=server.createEventHandler();
Now our server is finished.
You need to map:
- ApiGateway v2 events to
handleWebSockethandler - ApiGateway v1 events to
handleHTTP - DynamoDB stream from events table to
handleEvents.
In order to do that you can useServerless framework, seeserverless.yml file.
To connect to this server you can useApollo Client + subscriptions-transport-ws - see example in section 2
Sometime if you have complex schema you want to pass dependencies using context so it's easier to manage.
import{DynamoDBConnectionManager,DynamoDBEventProcessor,DynamoDBEventStore,DynamoDBSubscriptionManager,PubSub,Server,}from'aws-lambda-graphql';consteventStore=newDynamoDBEventStore();consteventProcessor=newDynamoDBEventProcessor();constsubscriptionManager=newDynamoDBSubscriptionManager();constconnectionManager=newDynamoDBConnectionManager({subscriptions:subscriptionManager,});constpubSub=newPubSub({ eventStore});consttypeDefs=/* GraphQL */` type Mutation { broadcastMessage(message: String!): String! } type Query { """ Dummy query so out server won't fail during instantiation """ dummy: String! } type Subscription { messageBroadcast: String! }`;constresolvers={Mutation:{broadcastMessage:async(root,{ message},ctx)=>{awaitctx.pubSub.publish('NEW_MESSAGE',{ message});returnmessage;},},Query:{dummy:()=>'dummy',},Subscription:{messageBroadcast:{resolve:(rootValue)=>rootValue.message,// subscribe works similarly as resolve in any other GraphQL type// pubSub.subscribe() returns a resolver so we need to pass it all the args as were received// so we can be sure that everything works correctly internallysubscribe:(rootValue,args,ctx,info)=>{returnctx.pubSub.subscribe('NEW_MESSAGE')(rootValue,args,ctx,info);},},},};constserver=newServer({// accepts all the apollo-server-lambda options and adds few extra options// provided by this packagecontext:{ pubSub,}, connectionManager, eventProcessor, resolvers, subscriptionManager, typeDefs,});exportconsthandleWebSocket=server.createWebSocketHandler();exportconsthandleHTTP=server.createHttpHandler();// this creates dynamodb event handler so we can send messages to subscribed clientsexportconsthandleEvents=server.createEventHandler();
First install dependencies
yarn add @apollo/client @apollo/link-ws subscriptions-transport-ws#ornpm install @apollo/client @apollo/link-ws subscriptions-transport-ws
import{WebSocketLink}from'@apollo/link-ws';import{SubscriptionClient}from'subscriptions-transport-ws';import{ApolloProvider,ApolloClient,InMemoryCache}from'@apollo/client';constwsClient=newSubscriptionClient('ws://localhost:8000',// please provide the uri of the api gateway v2 endpoint{lazy:true,reconnect:true},null,[],);constlink=newWebSocketLink(wsClient);constclient=newApolloClient({cache:newInMemoryCache(), link,});
To deploy your api created using this library please seeserverless.yml template of example app.
This library supportsserverless-offline. But you have to do small changes in your code to actually support it, it's not automatical.
You need to set up custom endpoint for ApiGatewayManagementApi and custom endpoint for DynamoDB if you're using it. Please refer tochat-example-server source code.
GraphQL client and server implementation for AWS Lambda.
Seepackage
Current infrastructure is implemented usingAWS Lambda + AWS API Gateway v2 +AWS DynamoDB (withDynamoDB streams). But it can be implemented using various solutions (Kinesis, etc) because it's written to be modular.
- Chat App - React app
- Chat Server
- contains AWS Lambda that handles HTTP, WebSocket and DynamoDB streams
- also includes
serverless.ymlfile for easy deployment
- This project uses TypeScript for static typing.
- This projects uses conventional commits.
- This project uses Yarn and Yarn workspaces so only
yarn.lockis commited. - Please add a Changelog entry under
Unreleasedsection in your PR.
yarn test#running testsin watch modeyarn test:watch
yarn buildRunning tests locally:
yarn testThanks goes to these wonderful people (emoji key):
This project follows theall-contributors specification. Contributions of any kind welcome!
MIT
About
Use AWS Lambda + AWS API Gateway v2 for GraphQL subscriptions over WebSocket and AWS API Gateway v1 for HTTP
Topics
Resources
License
Uh oh!
There was an error while loading.Please reload this page.
Stars
Watchers
Forks
Sponsor this project
Uh oh!
There was an error while loading.Please reload this page.
Packages0
Uh oh!
There was an error while loading.Please reload this page.
