With the release of AWS AppSync we finally have the ability to create realtime serverless apps on AWS. Previously you were forced to spin up EC2 instances in order to create websocket connections as they are not supported by AWS Lambda.
In the tutorial you’ll learn how to build a simple GraphQL API using AWS AppSync. Then you’ll write a client app in vanilla javascript (no frameworks) that receives realtime updates via websockets. Let’s get started!
- Set Up
- Create GraphQL Schema
- Create Mapping Templates
- Deploy AppSync GratphQL API with Serverless Framework
- Create GraphQL Queries For Client Application
- Download API Config from AWS Console
- Isomorphic Vanilla JavaScript Client Code For Subscribing to Realtime API Updates
- Node.js Client Application Build Process
- Node.js Client Application
- Browser Client Build Process
- Browser Client
- Conclusion
1. Setup
Go ahead and install the serverless framework cli which we will be using to deploy our AppSync GraphQL API and create a new directory for our project.
$ npm install -g serverless
$ mkdir realtime-chat
$ cd realtime-chat
2. Create GraphQL Schema
We will define a basic GraphQL schema for our chat application.
schema {
query: Query
mutation: Mutation
subscription: Subscription
}
type Subscription {
inbox(to: String): Page
@aws_subscribe(mutations: ["message"])
}
type Mutation {
message(body: String!, to: String!): Page!
}
type Message {
from: String!
to: String!
body: String!
sentAt: String!
}
type Query {
me: String
}
A standard GraphQL schema apart from the realtime subscription which uses a special syntax to indicate to AWS AppSync which mutation to subscribe to (@aws_subscribe(mutations: ["message"])
), in this case the message
mutation.
3. Create Mapping Templates
Now we have our schema defined we need to add resolvers for it. If you’re expecting to need to write a lambda function you’d be wrong! AppSync introduces the concept of mapping templates which translate the client request to one the backing store (DynamoDB, elasticsearch etc) understands and then translates the response back to the client again.
To keep things simple we are creating an API without a database. AppSync offers a special type of resolver called a local resolver which does not persist the request data but instead just relays it on to whatever subscribers exist at the time.
Let’s create a directory to house our mapping templates.
$ mkdir mapping-templates
Then let’s create the request template for our message
mutation in a file called mapping-templates/Message.request.vtl
which will extract the fields from the mutation request.
{
"version": "2017-02-28",
"payload": {
"body": "${context.arguments.body}",
"from": "${context.identity.username}",
"to": "${context.arguments.to}",
"sentAt": "$util.time.nowISO8601()"
}
}
For the response we just use the standard forwarding template. Create a file called mapping-templates/ForwardResult.response.vtl
with the following contents.
$util.toJson($context.result)
Your folder structure should now look like the following:
$ tree mapping-templates
mapping-templates
├── ForwardResult.response.vtl
└── Message.request.vtl
4. Deploy AppSync GraphQL API with Serverless Framework
Now we need to create a config file for the serverless framework to provision our API. In order to do this we’re going to use the Serverless-AppSync-Plugin.
Install it with npm
.
$ npm install --dev serverless-appsync-plugin
Then create a serverless.yml
file with the following contents.
---
service: realtime-chat
frameworkVersion: ">=1.21.0 <2.0.0"
plugins:
- serverless-appsync-plugin
provider:
name: aws
region: eu-west-1
custom:
awsAccountId: ${env:AWS_ACCOUNT_ID}
appSync:
name: realtimeChat
apiKey: ${env:APPSYNC_API_KEY}
apiId: ${env:APPSYNC_API_ID}
authenticationType: API_KEY
schema: schema/schema.graphql
serviceRole: "AppSyncServiceRole" # AppSyncServiceRole is a role defined by amazon and available in all accounts
mappingTemplatesLocation: mapping-templates
mappingTemplates:
- dataSource: Chat
type: Mutation
field: message
request: Message.request.vtl
response: ForwardResult.response.vtl
- dataSource: Chat
type: Subscription
field: inbox
request: Message.request.vtl
response: ForwardResult.response.vtl
dataSources:
- type: NONE # use an AppSync local resolver
name: Chat
description: 'Chat relay'
As you can see we set the data source type to NONE
in order to use the local resolver as we do not want to persist the chat messages in a database but instead just forward them to other clients listening for updates.
Our serverless.yml
config contains a few environment variables that we must supply. Let’s create a .env
file that contains our AWS Account ID and dynamically populates the other variables.
# .env
export AWS_ACCOUNT_ID=123456789
export APPSYNC_API_ID=$(aws appsync list-graphql-apis \
--query 'graphqlApis[?name==`realtimeChat`].apiId' \
--output text >/dev/null 2>&1)
export APPSYNC_API_KEY=$(aws appsync list-api-keys \
--api-id "$APPSYNC_API_ID" \
--query 'apiKeys[0].id' \
--output text >/dev/null 2>&1)
Now we’re ready to deploy our API with a single command:
$ .env && sls deploy
Congratulations! You’ve just deployed a GraphQL API with realtime support.
5. Create GraphQL Queries For Client Application
The next thing we need to do is to create the GraphQL queries that we will be using from our client to query our API.
First let’s create a directory to house our client code.
$ mkdir src
Then let’s create a directory for our queries.
$ mkdir src/graphql
Create a file at src/graphql/inboxSubscription.js
for our subscription query with the following contents:
import gql from 'graphql-tag';
export default gql`
subscription Inbox($to: String) {
inbox(to: $to) {
from
body
}
}`;
This is just a simple subscription query which will return from
and body
message fields.
6. Download API Config from AWS Console
We need to download the config settings for the app so that it can connect to our GraphQL API.
Navigate to AppSync section in the AWS Console. Select your API and download the web config settings.
You will have a file named AppSync.js
in your download area. Move this your src
directory and rename it to config.js
.
The config file you download should look like the following if you have left the default option for securing your API as API_KEY
.
export default {
"graphqlEndpoint": "https://xxxx.appsync-api.eu-west-1.amazonaws.com/graphql",
"region": "eu-west-1",
"authenticationType": "API_KEY",
"apiKey": "xxxxxxxxxxxxxxxxxxxxxxxxx"
}
7. Isomorphic Vanilla JavaScript Client Code For Subscribing to Realtime API Updates
We are going to create an isomorphic client - one that can be run either in the browser or via node.js in a terminal.
First let’s install the dependencies we’ll need.
$ npm install -s apollo-cache-inmemory apollo-client apollo-link aws-appsync aws-sdk es6-promise graphql graphql-cli graphql-tag isomorphic-fetch ws
Then let’s create an entrypoint for the application.
$ touch src/index.js
Your app source code directory should now have the following contents.
$ tree src
src
├── config.js
├── graphql
│ └── inboxSubscription.js
└── index.js
Paste the following code into your index.js
file:
const RECIPIENT = 'Bobby';
if (!global.WebSocket) {
global.WebSocket = require('ws');
}
if (!global.window) {
global.window = {
setTimeout: setTimeout,
clearTimeout: clearTimeout,
WebSocket: global.WebSocket,
ArrayBuffer: global.ArrayBuffer,
addEventListener: function () { },
navigator: { onLine: true }
};
}
if (!global.localStorage) {
global.localStorage = {
store: {},
getItem: function (key) {
return this.store[key];
},
setItem: function (key, value) {
this.store[key] = value;
},
removeItem: function (key) {
delete this.store[key];
}
};
}
require('es6-promise').polyfill();
require('isomorphic-fetch');
// Require config file downloaded from AWS console with endpoint and auth info
const AppSyncConfig = require('./config').default;
const AWSAppSyncClient = require('aws-appsync').default;
import InboxSubscription from './graphql/inboxSubscription';
// Set up Apollo client
const client = new AWSAppSyncClient({
url: AppSyncConfig.graphqlEndpoint,
region: AppSyncConfig.region,
auth: {
type: AppSyncConfig.authenticationType,
apiKey: AppSyncConfig.apiKey,
}
});
client.hydrated().then(function (client) {
const observable = client.subscribe({ query: InboxSubscription, variables: { to: RECIPIENT } });
const realtimeResults = function realtimeResults(data) {
console.log('realtime data: ', data);
};
observable.subscribe({
next: realtimeResults,
complete: console.log,
error: console.log,
});
});
For this simple demo we have hardcoded the recipient to Bobby
but obviously you’d want to make this dynamic for a real application.
8. Node.js Client Application Build Process
At this point we have all the source code written for our websockets client, we just need to implement the build process. Because the source code uses es6 features we need to transpile it using babel.
Install the dev dependencies we’ll need.
$ npm install --save-dev babel-cli babel-preset-es2015 rimraf webpack webpack-cli webpack-dev-server
Now let’s build our application.
$ rimraf build/ && babel ./src --out-dir build/ --ignore ./node_modules,./.babelrc,./package.json,./npm-debug.log --copy-file
9. Node.js Client Application
Now let’s run our application in node.js.
$ node build/index.js
Navigate to the queries
page in your AppSync API in the AWS console and run the following graphQL mutation to trigger some udpates.
mutation Message {
message(to: "Bobby", body: "Yo node!") {
body
to
from
sentAt
}
}
You should see the message appear immediately in your terminal.
10. Browser Client Application Build Process
Remember the code we’ve written is isomorphic. That means it’ll run just as well in the browser.
First install the dev dependencies we’ll need.
$ npm install --save-dev webpack webpack-cli webpack-dev-server
We’re going to use webpack to run our build process so we need to create a config file for it. Create a file called webpack.config.js
at the route of the project with the following contents.
const path = require('path');
module.exports = {
entry: './src/index.js',
output: {
filename: 'bundle.js',
path: path.resolve(__dirname, 'dist')
}
};
As the webserver will be serving assets from the dist
directory we need to add an index.html
file that includes the bundle.js
file which webpack will generate. Create a file at dist/index.html
with the following contents.
<!DOCTYPE html>
<html>
<head>
<title>AWS Serverless Websockets Demo</title>
</head>
<body>
</body>
<script type="text/javascript" src="bundle.js"></script>
</html>
10. Browser Client Application
Run the following command which will start webpack-dev-server
serving assets from the dist
directory.
$ webpack-dev-server --mode development --content-base dist/
Then navigate to http://localhost:8080
in your browser and open the dev tools as we’ll be logging the data to the console.
Navigate to the queries
page in your AppSync API in the AWS console and run another graphQL mutation to trigger an update.
mutation Message {
message(to: "Bobby", body: "hello browser!") {
body
to
from
sentAt
}
}
You should see the updates being logged to your browser’s console.
12. Conclusion
We’ve set up a serverless GraphQL API with node.js and browser clients consuming realtime updates via websockets. Not bad for <30 minutes work!
Although AppSync is promoted as a managed GraphQL API service, for me its best feature is the ability to serve realtime updates. Previously you would have had to run a server 24/7 to do this. Now you get all the cost savings of serverless and without any of the headaches of managing servers.
Full source code for this serverless websockets example available on github.
If you’re interested in learning more about building realtime serverless applications then check out my upcoming training course Full Stack Serverless GraphQL Apps on AWS AppSync.