GraphQL is a specification for which there are many different kinds of implementations in the market. AppSync is a serverless implementation of GraphQL by AWS and is a managed GraphQL platform.

As mentioned in this article, AppSync can be a way to replace the API Gateway + AWS Lambda pattern for connecting clients to your serverless backends. 

In this post, we’ll start with AppSync by creating a simple GraphQL application with two data sources: DynamoDB and AWS Lambda. But first, let’s learn a bit more about GraphQL.

GraphQL – Some Background

GraphQL is a different way to connect your client applications to your backend. In general, we are used to REST, where the client connects to the backend via a defined endpoint. When connecting to an endpoint, the client needs to specify headers, an HTTP method (GET, POST, DELETE, etc.), a body, and all kinds of parameters.

Usually, he needs to be familiar with all the information beforehand to be able to request the information from the server. The client might end up getting a lot of information back that isn’t necessary (over fetch) and often forces him to do a lot of calls to the backend to populate a view since one request only brings part of the information necessary (under fetch).

GraphQL has some important characteristics. Between the client and the server, a contract is signed, called the GraphQL schema. Here, all possible types of operations that the client can perform on the backend are defined.

The client can call an operation with one request, which will return multiple types if needed so that there is no under fetch of information. The client can also specify attributes for the types it wants so that there is no over fetch of information. 

GraphQL is implemented between the client and the data sources, making GraphQL a single entry point to the backend. The data sources can be anything from NoSQL databases, relational databases, and HTTP servers to whatever returns data. To connect a GraphQL implementation to the data sources, you need to write resolvers.

Resolvers are a very important part of your GraphQL application, as they are the ones that translate GraphQL requests or responses to whatever the data sources understand. For example, if the data source is a relational database, the resolver will need to know how to transform a GraphQL query into a SELECT operation and then translate whatever the relational database returns into a GraphQl response.

Introduction to AppSync

AppSync can do everything GraphQL specifies and also has additional features that come out of the box. For example, it supports authentication and authorization. This enables to filter the data you return and the operations that the clients can perform, depending on which user is in.

AppSync supports Cognito, API Key, IAM permissions, and Open ID Connect and even provides out-of-the-box support for DynamoDB, AWS Lambda, Elasticsearch, HTTP, and RDS as data sources.

Appsync also has support for real-time operations. This means that if a data source is updated, the clients that are subscribed to these operations will get updated as well. This way, the clients don’t need to ping the GraphQL service to check if there is new data available.

Also, there is offline support, meaning that if the client application goes offline and modifies the data when it comes online, the data will get synced with the data sources automatically. 

Read about all of AppSync’s features in this document and understand the pricing better.

Monitor and troubleshoot AppSync with Epsagon today!

Start With AppSync

As mentioned, we’ll be using DynamoDB and AWS Lambda to create our GraphQL application. The application will store image metadata in DynamoDB. The images will be in S3, and we’ll save an identifier in DynamoDB for each image and the S3 path.

Then, when we want to retrieve an image, it will fetch the S3 path with the image identifier. Using AWS Lambda, we will then get the signed URL for that image so that the client can see it.

AppSync Architecture

AppSync Architecture

When getting started with a new serverless application, you should first read this blog post, which covers some basic considerations for a beginner serverless developer.

We’re going to start with a Serverless Framework and implement a serverless framework plugin to use AppSync easily in our project. Before starting this tutorial, you will also have to make sure your computer has an AWS account and Serverless Framework installed and configured.

Then, you can go ahead and create a new Serverless Framework project. For that, we need to run this command inside an empty directory: 

$ sls create --template aws-nodejs --name appsync-intro

This will create the boilerplate for our project–the serverless.yml and handler.js files.

Now go to the serverless.yml file and edit it like this:

service: appsync-intro

plugins:
  - serverless-appsync-plugin
  - serverless-pseudo-parameters

provider:
 name: aws
 runtime: nodejs10.x

functions:
 graphql:
   handler: handler.graphql

For this tutorial, you’ll use the serverless-appsync-plugin and the serverless-pseudo-parameters. For the plugins to work, install them into your project. In order to do that, type this into your terminal:

$ npm install --save serverless-appsync-plugin
$ npm install --save serverless-pseudo-parameters

Now you can start setting up the AppSync application. Write the following code into the serverless.yml after the provider and before the functions:

custom:     
 IMAGE_TABLE: appsync-intro-image-table
 BUCKET_NAME: <your-bucket-name>
 appSync:
   name:  appsync-intro
   authenticationType: API_KEY
   mappingTemplates:
     - dataSource: Images
       type: Mutation
       field: saveImage
       request: saveImage-request-mapping-template.vtl
       response: saveImage-response-mapping-template.vtl
     - dataSource: lambdaDatasource
       type: Query
       field: getImageSignedPath
       request: getImageSignedPath-request-mapping-template.vtl
       response: getImageSignedPath-response-mapping-template.vtl
   dataSources:
     - type: AMAZON_DYNAMODB
       name: Images
       description: 'Table containing the metadata of the images'
       config:
         tableName: { Ref: ImageTable }
         iamRoleStatements:
           - Effect: "Allow"
             Action:
               - "dynamodb:GetItem"
               - "dynamodb:PutItem"
             Resource:
               - "arn:aws:dynamodb:#{AWS::Region}:*:table/${self:custom.IMAGE_TABLE}"
               - "arn:aws:dynamodb:#{AWS::Region}:*:table/${self:custom.IMAGE_TABLE}/*"
     - type: AWS_LAMBDA
       name: lambdaDatasource
       description: 'Lambda DataSource'
       config:
         functionName: graphql
         iamRoleStatements: # custom IAM Role statements for this DataSource. Ignored if `serviceRoleArn` is present. Auto-generated if both `serviceRoleArn` and `iamRoleStatements` are omitted
           - Effect: "Allow"
             Action:
               - "lambda:invokeFunction"
             Resource:
               - "arn:aws:lambda:#{AWS::Region}:#{AWS::AccountId}:function:${self:service}-dev-graphql"
               - "arn:aws:lambda:#{AWS::Region}:#{AWS::AccountId}:function:${self:service}-dev-graphql:*"

The first two lines are just defining environmental variables with the name of the bucket and the DynamoDB table We’ll use them to store the metadata.

Now, we can start configuring the AppSync application with the following details:

  • Name: The name of the AppSync application.
  • Authentication Type: How we want to secure the AppSync app. In this case, you will use the simplest way of securing the app–with an API Key. 
  • Mapping Templates: To define all the different resolvers for your application. For each operation or field defined in your schema.graphql, you need to specify the resolvers–one resolver for each request and one resolver for each response. Resolvers in AppSync are written in VTL (velocity template language).
  • Data sources: To define all the different data sources for your application. You have two data sources in this application: DynamoDB and Lambda. In this section, you will specify the type of data source, the resource name or ARN (Amazon resource name), and the permissions that you will give AppSync to operate over this data source.

After everything is defined above, we’ll create the Table and also give permissions to the function to fetch items from the Table and to retrieve items from the S3 bucket. For this, we’ll need to add the following code after the provider and before the custom property in the serverless.yml:

iamRoleStatements:
   - Effect: "Allow"
     Action:
       - "s3:ListBucket"
       - "s3:GetObject"
     Resource: "arn:aws:s3:::${self:custom.BUCKET_NAME}/*"
   - Effect: "Allow"
     Action:
       - "dynamodb:GetItem"
     Resource: "arn:aws:dynamodb:#{AWS::Region}:*:table/${self:custom.IMAGE_TABLE}"

And then at the end of the file, add: 

resources:
 Resources:
   #Image table
   ImageTable:
     Type: "AWS::DynamoDB::Table"
     Properties:
       KeySchema:
         - AttributeName: name
           KeyType: HASH
       AttributeDefinitions:
         - AttributeName: name
           AttributeType: S
       BillingMode: PAY_PER_REQUEST
       TableName: ${self:custom.IMAGE_TABLE}

Your serverless.yml is almost ready, but we still need to define the schema.graphql file for your application. So, create a new file and name it “schema.graphql.” There we can add: 

type Query {
    getImageSignedPath(imageName: String!): String!
}
type Mutation {
    saveImage(name: String!, s3Path: String!): Image!
}
type Image {
    name: String!
    s3Path: String!
}
schema {
    query: Query
    mutation: Mutation
}

This will add two operations and one type to your application. The type is called Image and has a name and S3Path that will refer to the place where it is stored in AWS S3. The query is the operation without side effects and is called getImageSignedPath.

When given an image name, the query returns a signed URL for the image. The mutation is the operation that performs side effects in your backend and is called saveImage. When given an image name and S3Path, the mutation stores the image metadata in the DynamoDB image table.  

Next, we’ll create the resolver files by specifying the four different resolvers in your serverless.yml–one each for the request and the response of the operation getImageSignedPath as well as for the request and the response of the operation saveImage. The resolvers are very simple, and you can find the files in this link

Finally, write the business logic for your function. Go ahead and write the following code in your handler.js:

'use strict';
const AWS = require('aws-sdk');
const s3 = new AWS.S3({signatureVersion: 'v4'});
const dynamo = new AWS.DynamoDB.DocumentClient();
module.exports.graphql = async (event) => {
 switch (event.field) {
   case 'getImageSignedPath': {
     const bucket = <your own bucket name>;
     const imageName = event.arguments.imageName;     
     const key = await getImageS3Path(imageName);
     return signURL(bucket, key);
   }
   default: {
     return `Unknown field, unable to resolve ${event.field}`, null;
   }
 }
};
function signURL(bucket, key) {
 const params = {'Bucket': bucket, 'Key': key};
 return s3.getSignedUrl('getObject', params);
}
async function getImageS3Path(imageName) {
 const params = {
   Key: {
      name: imageName
   },
   TableName:'appsync-intro-image-table'
   };
  return dynamo.get(params).promise().then(result => {
   return result.Item.s3Path;
 });
}

This function is a very simple one that, given an image name that is passed in the arguments of the request, returns a signed URL. It first fetches the image S3 path from the image metadata table. Then, when it gets the path, it uses the AWS SDK S3 module to retrieve the signed URL and finally returns that URL as a response. 

You can find all the code for this application here.

Testing

To test this application, simply deploy it from the terminal: 

$ sls deploy

This will deploy and create all the resources you need–DynamoDB table, AppSync application, and function. Then, in your AWS account, simply create an S3 bucket with the name that you specified in the environmental variables. Here, you can store some images.

Now go to the AppSync console, then to Queries. There you can create some queries to test the application. For example, if you stored an image with the path image1.png, you can type this mutation there.

AppSync Queries Console

AppSync Queries Console

If you want to fetch the signed path, you can type this query:

Query Image Path

Query Image Path

Integrating the API With a Client

If you want to use this API in a client application, use the Amplify Library, which builds cloud-native applications. Via this library, you can easily connect with the API and perform queries and mutations on it.

Monitoring With Epsagon

Epsagon’s tracing captures AppSync triggers, as seen in the image below. Get started in less than 5 minutes.

Epsagon tracing AppSync Triggers

Epsagon tracing AppSync Triggers

Conclusion

AppSync is a different way to create APIs than REST. By using GraphQL and a managed platform, you can ensure extremely fast development for your client backends. You can also sync multiple entry points in the backend instead of giving the responsibility for orchestrating all backend calls to the client application.

AppSync can be a great tool when you want to implement a customer-facing web app that has a complex model. You can also easily define the schema and then connect it to the different data sources.

Using managed services helps speed up application development and allows you to not have to maintain unnecessary code and infrastructure. Aside from AppSync, there are many other useful managed services in AWS that you can take a look at as well, such as Step Functions and Athena.

Monitor and troubleshoot AppSync with Epsagon today!