Download as pdf or txt
Download as pdf or txt
You are on page 1of 17

Blog Home Category   Edition   Follow   Search Blogs 

AWS Mobile Blog

Building Scalable GraphQL APIs on AWS with CDK,


TypeScript, AWS AppSync, Amazon DynamoDB, and AWS
Lambda
by Nader Dabit | on 23 SEP 2020 | in Amazon DynamoDB, AWS Amplify, AWS AppSync, AWS Mobile Development, Mobile
Services, Top Posts | Permalink |  Comments |   Share

    https://aws.amazon.com/blogs/mobile/building-scalable-gra

AWS AppSync is a managed serverless GraphQL service that simplifies application development by letting
you create a flexible API to securely access, manipulate, and combine data from one or more data sources
with a single network call and API endpoint. With AppSync, developers can build scalable applications on a
range of data sources, including Amazon DynamoDB NoSQL tables, Amazon Aurora Serverless relational
databases, Amazon Elasticsearch clusters, HTTP APIs, and serverless functions powered by AWS Lambda.

AppSync APIs can be deployed in a variety of different ways using various CloudFormation providers like the
Amplify CLI, SAM, CDK, and the Serverless Framework (among others).

In this post, we’ll be building an AWS AppSync API from scratch using CDK. The post will focus on how to use
CDK to deploy AppSync APIs that leverage a variety of AWS services including Amazon DynamoDB and AWS
Lambda.

The API we will be deploying will be leveraging a DynamoDB database for creating, reading, updating, and
deleting data. We’ll learn how to map GraphQL requests to DynamoDB using Direct Lambda resolvers. We’ll
also learn how to enable GraphQL subscriptions for real-time updates triggered from database events.

The final code for this project is located here.

CDK Overview

The AWS Cloud Development Kit (AWS CDK) is an open source software development framework to model
and provision your cloud application resources using familiar programming languages. CDK can be written
using a variety of programming languages like Python, Java, TypeScript, JavaScript, and C#. In this tutorial
we will be using TypeScript

To work with CDK, you will need to install the CDK CLI. Once the CLI is installed, you will be able to do things
like create new CDK projects, deploy services to AWS, deploy updates to existing services, and view changes
made to your infrastructure during the development process.
In this post we’ll be using the CDK CLI to provision and deploy API updates.

CDK with AppSync

An AppSync API is usually composed of various AWS services. For example, if we are building an AppSync API
that interacts with a database and needs authorization, the API will depend on these resources being created.

With this in mind, we will not only be working with CDK modules for AppSync but also various other AWS
services. Part of building an AppSync API is learning how to use all of these things and making them work
well together, including managing IAM policies in certain circumstances in order to enable access between
services or to configure certain types of data access patterns.

The AppSync CDK constructs and classes take this into consideration and enable the configuration of various
resources within an AppSync API using AWS services also created as part of the CDK project.

Click here to view the CDK documentation. Click here to view the AWS AppSync
documentation.

Getting Started

First, install the CDK CLI:

Bash
npm install -g aws-cdk

You must also provide your credentials and an AWS Region to use AWS CDK, if you have not already done so.
The easiest way to satisfy this requirement is to install the AWS CLI and issue the following command:

Bash
aws configure

Next, create a directory called appsync-cdk-app and change into the new directory:

Bash
mkdir appsync-cdk-app

cd appsync-cdk-app

Next, we’ll create a new CDK project using the CDK CLI:

Bash
cdk init --language=typescript
The CDK project should now be initialized and you should see a few files in the directory, including a lib
folder which is where the boilerplate for the root stack has been created.

Now that we’ve created the CDK project, let’s install the necessary dependencies we’ll need. Since we’ll be
working with several different packages, we’ll need to go ahead and install them now:

Bash
npm install @aws-cdk/aws-appsync @aws-cdk/aws-lambda @aws-cdk/aws-dynamodb

Running a build
Because the project is written in TypeScript, but will ultimately need to be deployed in JavaScript, you will
need to create a build to convert the TypeScript into JavaScript before deploying.

There are two ways to do this, and the project is already set up with a couple of scripts to help with this.

Watch mode

You can run npm run watch to enable watch mode. The project will automatically compile to JS as soon
as you make changes and save files and you will also be able to see any errors logged out to the terminal.

Manual build

You can also create a build at any time by running npm run build .

Creating the API

Now that the project is set up, we can start writing some code! Some boilerplate code for the stack has
already been created for you. This code is located at appsync-cdk-app/lib/appsync-cdk-app-stack.ts. This is
the root of the CDK app and where we will be writing the code for our app.

To get started, let’s first go ahead and import the CDK modules we’ll be needing:

TypeScript
// lib/appsync-cdk-app-stack.ts
import * as cdk from '@aws-cdk/core';
import * as appsync from '@aws-cdk/aws-appsync';
import * as ddb from '@aws-cdk/aws-dynamodb';
import * as lambda from '@aws-cdk/aws-lambda';

Next we’ll use the appsync CDK module to create the API. Update the Stack with following code:

TypeScript
// lib/appsync-cdk-app-stack.ts
export class AppsyncCdkAppStack extends cdk.Stack {
constructor(scope: cdk.Construct, id: string, props?: cdk.StackProps) {
super(scope, id, props);
// Creates the AppSync API
const api = new appsync.GraphqlApi(this, 'Api', {
name: 'cdk-notes-appsync-api',
schema: appsync.Schema.fromAsset('graphql/schema.graphql'),
authorizationConfig: {
defaultAuthorization: {
authorizationType: appsync.AuthorizationType.API_KEY,
apiKeyConfig: {
expires: cdk.Expiration.after(cdk.Duration.days(365))
}
},
},
xrayEnabled: true,
});

// Prints out the AppSync GraphQL endpoint to the terminal


new cdk.CfnOutput(this, "GraphQLAPIURL", {
value: api.graphqlUrl
});

// Prints out the AppSync GraphQL API key to the terminal


new cdk.CfnOutput(this, "GraphQLAPIKey", {
value: api.apiKey || ''
});

// Prints out the stack region to the terminal


new cdk.CfnOutput(this, "Stack Region", {
value: this.region
});
}
}

We’ve defined a basic AppSync API with the following configuration:

name: Defines the name of the AppSync API


schema: Specifies the location of the GraphQL schema
authorizationConfig: This allows you to define the default authorization mode its configuration, as
well as (optional) additional authorization modes
xrayEnabled: Enables xray debugging

Next, we need to define the GraphQL schema. Create a new folder in the root of the project named graphql
and within it, create a new file named schema.graphql:

GraphQL
# graphql/schema.graphql
type Note {
id: ID!
name: String!
completed: Boolean!
}

input NoteInput {
id: ID!
name: String!
completed: Boolean!
}

input UpdateNoteInput {
id: ID!
name: String
completed: Boolean
}

type Query {
getNoteById(noteId: String!): Note
listNotes: [Note]
}

type Mutation {
createNote(note: NoteInput!): Note
updateNote(note: UpdateNoteInput!): Note
deleteNote(noteId: String!): String
}

This schema defines a notes app with two queries and three mutations for basic CRUD + List functionality.

Now, or any time, we can run the CDK diff command to see what changes will be deployed:

Bash
cdk diff

The diff command compares the current version of a stack defined in your app with the already-deployed
version and displays a list of differences.

From here, we can build and deploy the API to see it in the AppSync console. To do so, run the following
command from your terminal:

Bash
npm run build && cdk deploy

When the build finishes, you should see JavaScript files compiled from the TypeScript files in your project.

Once the deployment is complete, you should be able to see the API (cdk-notes-appsync-api) in the AppSync
console.

🎉 Congratulations, you’ve successfully deployed an AppSync API using CDK!


Adding a Lambda Data Source

Now that we’ve created the API, we need a way to connect the GraphQL operations (createNote,
updateNote, listNotes, etc..) to a data source. We will be doing this by mapping the operations into a
Lambda function that will be interacting with a DynamoDB table.

To build out this functionality, we’ll next need to create the Lambda function and then add it as a data source
to the AppSync API. Add the following code below the API definition in lib/appsync-cdk-app-stack.ts:

TypeScript
// lib/appsync-cdk-app-stack.ts
const notesLambda = new lambda.Function(this, 'AppSyncNotesHandler', {
runtime: lambda.Runtime.NODEJS_12_X,
handler: 'appsync-ds-main.handler',
code: lambda.Code.fromAsset('lambda-fns'),
memorySize: 1024
});

// Set the new Lambda function as a data source for the AppSync API
const lambdaDs = api.addLambdaDataSource('lambdaDatasource', notesLambda);

Next, create a new folder called lambda-fns in the root of your project.

Now, when you run npm run build && cdk diff you should see there is a new Lambda function that is
enabled for your API.

Attaching the GraphQL resolvers


Now that the Lambda DataSource has been created, we need to enable the Resolvers for the GraphQL
operations to interact with the data source. To do so, we can add the following code below the Lambda data
source definition:

GraphQL
// lib/appsync-cdk-app-stack.ts
lambdaDs.createResolver({
typeName: "Query",
fieldName: "getNoteById"
});

lambdaDs.createResolver({
typeName: "Query",
fieldName: "listNotes"
});

lambdaDs.createResolver({
typeName: "Mutation",
fieldName: "createNote"
});
lambdaDs.createResolver({
typeName: "Mutation",
fieldName: "deleteNote"
});

lambdaDs.createResolver({
typeName: "Mutation",
fieldName: "updateNote"
});

Adding a DynamoDB Table


Now that the Lambda function has been configured we need to create a DynamoDB table and enable the
Lambda function to access it.

To do so, we’ll add the following code below the GraphQL resolver definitions in lib/appsync-cdk-app-
stack.ts:

TypeScript
// lib/appsync-cdk-app-stack.ts
const notesTable = new ddb.Table(this, 'CDKNotesTable', {
billingMode: ddb.BillingMode.PAY_PER_REQUEST,
partitionKey: {
name: 'id',
type: ddb.AttributeType.STRING,
},
});
// enable the Lambda function to access the DynamoDB table (using IAM)
notesTable.grantFullAccess(notesLambda)

// Create an environment variable that we will use in the function code


notesLambda.addEnvironment('NOTES_TABLE', notesTable.tableName);

We’ve now created a DynamoDB table, enabled the Lambda function to access it by configuring IAM
permissions, and created an environment variable referencing the DynamoDB table name that we will use in
the Lambda function.

Adding the Lambda function code


Finally we need to add the function code that we will be using to map the GraphQL queries and mutations
into DynamoDB operations.

We can use the event object that is passed into the function argument to find the fieldName of the
operation that is invoking the function and then map the invocation to the proper DynamoDB operation.
For example, if the GraphQL operation that triggered the function is createNote , then the fieldName
will be available at event.info.fieldName .

We can also access any arguments that are passed into the GraphQL operation by accessing the
event.arguments object.

With this in mind, create the following files in the lambda-fns directory: appsync-ds-main.ts, createNote.ts,
deleteNote.ts, getNoteById.ts, listNotes.ts, updateNote.ts, and Note.ts.

Next, we’ll update these files with the code needed to interact with the DynamoDB table and perform various
operations.

appsync-ds-main.ts

TypeScript
// lambda-fns/appsync-ds-main.ts

import createNote from './createNote';


import deleteNote from './deleteNote';
import getNoteById from './getNoteById';
import listNotes from './listNotes';
import updateNote from './updateNote';
import Note = require('./Note');

type AppSyncEvent = {
info: {
fieldName: string
},
arguments: {
noteId: string,
note: Note
}
}

exports.handler = async (event:AppSyncEvent) => {


switch (event.info.fieldName) {
case "getNoteById":
return await getNoteById(event.arguments.noteId);
case "createNote":
return await createNote(event.arguments.note);
case "listNotes":
return await listNotes();
case "deleteNote":
return await deleteNote(event.arguments.noteId);
case "updateNote":
return await updateNote(event.arguments.note);
default:
return null;
}
}
Note.ts

TypeScript
// lambda-fns/Note.ts
interface Note {
id: string;
name: string;
completed: boolean;
}

export = Note

createNote.ts

TypeScript
// lambda-fns/createNote.ts
const AWS = require('aws-sdk');
const docClient = new AWS.DynamoDB.DocumentClient();
import Note = require('./Note');

async function createNote(note: Note) {


const params = {
TableName: process.env.NOTES_TABLE,
Item: note
}
try {
await docClient.put(params).promise();
return note;
} catch (err) {
console.log('DynamoDB error: ', err);
return null;
}
}

export default createNote;

deleteNote.ts

TypeScript
// lambda-fns/deleteNote.ts
const AWS = require('aws-sdk');
const docClient = new AWS.DynamoDB.DocumentClient();

async function deleteNote(noteId: String) {


const params = {
TableName: process.env.NOTES_TABLE,
Key: {
id: noteId
}
}
try {
await docClient.delete(params).promise()
return noteId
} catch (err) {
console.log('DynamoDB error: ', err)
return null
}
}

export default deleteNote;

getNoteById.ts

TypeScript
// lambda-fns/appsync-ds-main.ts

const AWS = require('aws-sdk');


const docClient = new AWS.DynamoDB.DocumentClient();

async function getNoteById(noteId: String) {


const params = {
TableName: process.env.NOTES_TABLE,
Key: { id: noteId }
}
try {
const { Item } = await docClient.get(params).promise()
return Item
} catch (err) {
console.log('DynamoDB error: ', err)
}
}

export default getNoteById

listNotes.ts

TypeScript
// lambda-fns/listNotes.js
const AWS = require('aws-sdk');
const docClient = new AWS.DynamoDB.DocumentClient();

async function listNotes() {


const params = {
TableName: process.env.NOTES_TABLE,
}
try {
const data = await docClient.scan(params).promise()
return data.Items
} catch (err) {
console.log('DynamoDB error: ', err)
return null
}
}

export default listNotes;

updateNote.ts

TypeScript
// lambda-fns/updateNote.ts
const AWS = require('aws-sdk');
const docClient = new AWS.DynamoDB.DocumentClient();

interface Params {
TableName: string | undefined,
Key: string | {},
ExpressionAttributeValues: any,
ExpressionAttributeNames: any,
UpdateExpression: string,
ReturnValues: string
}

async function updateNote(note: any) {


let params : Params = {
TableName: process.env.NOTES_TABLE,
Key: {
id: note.id
},
ExpressionAttributeValues: {},
ExpressionAttributeNames: {},
UpdateExpression: "",
ReturnValues: "UPDATED_NEW"
};
let prefix = "set ";
let attributes = Object.keys(note);
for (let i=0; i<attributes.length; i++) {
let attribute = attributes[i];
if (attribute !== "id") {
params["UpdateExpression"] += prefix + "#" + attribute + " = :" + attribute
params["ExpressionAttributeValues"][":" + attribute] = note[attribute];
params["ExpressionAttributeNames"]["#" + attribute] = attribute;
prefix = ", ";
}
}
console.log('params: ', params)
try {
await docClient.update(params).promise()
return note
} catch (err) {
console.log('DynamoDB error: ', err)
return null
}
}

export default updateNote;

Deploying and testing


Now we are ready to deploy. To do so, run the following command from your terminal:

Bash
npm run build && cdk deploy

Now that the updates have been deployed, visit the AppSync console and click on the API name to view the
dashboard for your API.

Next click on Queries in the left hand menu to view the query editor. From here, we can test out the API by
running the following queries and mutations:

GraphQL
mutation createNote {
createNote(note: {
id: "001"
name: "My note"
completed: false
}) {
id
name
completed
}
}

query getNoteById {
getNoteById(noteId: "001") {
id
name
completed
}
}
query listNotes {
listNotes {
id
name
completed
}
}

mutation updateNote {
updateNote(note: {
id: "001"
completed: true
}) {
id
completed
}
}

mutation deleteNote {
deleteNote(noteId: "001")
}

Subscriptions
One of the most powerful components of a GraphQL API and one of the things that AppSync makes really
easy is enabling real-time updates via GraphQL subscriptions.

GraphQL subscriptions allow you to subscribe to mutations made agains a GraphQL API. In our case, we may
want to subscribe to changes like when a new note is created, when a note is deleted, or when a note is
updated. To enable this, we can update the GraphQL schema with the following Subscription definitions:

GraphQL
# graphql/schema.graphql
type Subscription {
onCreateNote: Note
@aws_subscribe(mutations: ["createNote"])
onDeleteNote: String
@aws_subscribe(mutations: ["deleteNote"])
onUpdateNote: Note
@aws_subscribe(mutations: ["updateNote"])
}

Next, deploy the changes:


Bash
cdk deploy

To test out the changes, open another window and visit the AppSync Query editor for the GraphQL API.

From here, you can test out the subscription by running a subscription in one editor and triggering a
mutation in another:

GraphQL
subscription onCreate {
onCreateNote {
id
name
completed
}
}

Connecting a Client application


You can connect to an AppSync API using the Amplify libraries for iOS, Android, or JavaScript.

In this example, we’ll walk through how you can make API calls from a JavaScript application.

Client project setup

You first need to install the AWS Amplify libraries using either NPM or Yarn.

Bash
npm install aws-amplify

Next, configure the Amplify app at the root of your project using the Project Region, API Key, and GraphQL
URL. This information is available both in the AppSync dashboard for your API but also in the terminal after
running cdk deploy . This configuration is usually done at the entry-point of your app:

Angular – main.ts
Vue – main.js 
React – index.js
Next.js – _app.js

JavaScript
import Amplify from 'aws-amplify';
Amplify.configure({
aws_appsync_region: "us-east-1", // Stack region
aws_appsync_graphqlEndpoint: "https://<app-id>.appsync-api.<region>.amazonaws.c
aws_appsync_authenticationType: "API_KEY", //Primary AWS AppSync authentication
aws_appsync_apiKey: "<YOUR_API_KEY>" // AppSync API Key
});
Fetching data (queries)

To query data, you can use the API category, passing in the query that you’d like to use. In our case, we’ll use
the same query from above:

JavaScript
import { API } from 'aws-amplify'

const query = `
query listNotes {
listNotes {
id name completed
}
}
`

async function fetchNotes(){


const data = await API.graphql({ query })
console.log('data from GraphQL:', data)
}

Updating data (mutations)

To create, update, or delete data, you can use the API category, passing in the mutation that you’d like to use
along with any variables. In this example, we’ll look at how to create a new note:

JavaScript
import { API } from 'aws-amplify'

const mutation = `
mutation createNote(note: NoteInput!) {
createNote(note: $note) {
id name completed
}
}
`

async function createNote() {


await API.graphql({
query: mutation,
variables: { note: { id: '001', name: 'Note 1', completed: false } }
})
console.log('note successfully created!')
}

Real-time data (subscriptions)


To subscribe to real-time updates, we’ll use the API category and pass in the subscription we’d like to listen
to. Any time a new mutation we are subscribed to happens, the data will be sent to the client application in
real-time.

JavaScript
import { API } from 'aws-amplify'

const subscription = `
subscription onCreateNote {
onCreateNote {
id name completed
}
}
`

function subscribe() {
API.graphql({
query: subscription
})
.subscribe({
next: noteData => {
console.log('noteData: ', noteData)
}
})
}

Conclusion
If you’d like to continue building this example out, you may look at implementing things like additional data
sources, argument-based subscriptions, or creating and then querying against a DynamoDB Global Secondary
Index.

If you’d like to see the code for this project, it is located here.

Resources
AWS AppSync
AWS Device Farm
Amazon Pinpoint
AWS Amplify
Follow
  AWS for Mobile
  Facebook
  LinkedIn
  Twitch
  Email Updates

Related Posts

How to delete user data in an AWS data lake

Instantly monitor serverless applications with AWS Resource Groups

Introducing immutable class mapping for the Enhanced DynamoDB Client in AWS SDK for Java 2.x

Build a compelling eCommerce experience with Amazon Interactive Video Service

Using the Amazon Chime SDK for 3rd party devices

Provisioning a Virtual Private Cloud at Scale with AWS CDK

Simplify Prior Authorization in Healthcare with AWS and HL7 FHIR

Unified serverless streaming ETL architecture with Amazon Kinesis Data Analytics

You might also like