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!

  1. Set Up
  2. Create GraphQL Schema
  3. Create Mapping Templates
  4. Deploy AppSync GratphQL API with Serverless Framework
  5. Create GraphQL Queries For Client Application
  6. Download API Config from AWS Console
  7. Isomorphic Vanilla JavaScript Client Code For Subscribing to Realtime API Updates
  8. Node.js Client Application Build Process
  9. Node.js Client Application
  10. Browser Client Build Process
  11. Browser Client
  12. 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. AppSync Config Export Screen

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
    }
}

Query for realtime updates in node.js application

You should see the message appear immediately in your terminal. Realtime updates in node.js application

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
    }
}

Query for realtime updates in browser application

You should see the updates being logged to your browser’s console. Realtime updates in browser application

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.