How to create a serverless GraphQL API with AWS AppSync
In this example we’ll look at how to create anAppSync GraphQL API on AWS usingSST. We’ll be allowing our users to get, create, update, delete, and list notes.
We’ll be using SST’sLive Lambda Development. It allows you to make changes and test AppSync locally without having to redeploy.
Here is a video of it in action.
Requirements
- Node.js 16 or later
- We’ll be using TypeScript
- AnAWS account with theAWS CLI configured locally
Create an SST app
Let’s start by creating an SST app.
$npx create-sst@latest--template=base/example graphql-appsync$cdgraphql-appsync$npminstallBy default, our app will be deployed to theus-east-1 AWS region. This can be changed in thesst.config.ts in your project root.
import{SSTConfig}from"sst";exportdefault{config(_input){return{name:"graphql-appsync",region:"us-east-1",};},}satisfiesSSTConfig;Project layout
An SST app is made up of two parts.
stacks/— App InfrastructureThe code that describes the infrastructure of your serverless app is placed in the
stacks/directory of your project. SST usesAWS CDK, to create the infrastructure.packages/functions/— App CodeThe code that’s run when your API is invoked is placed in the
packages/functions/directory of your project.
Setting up our infrastructure
Let’s start by defining our AppSync API.
Replace thestacks/ExampleStack.ts with the following.
import{StackContext,Table,AppSyncApi}from"sst/constructs";exportfunctionExampleStack({stack}:StackContext){// Create a notes tableconstnotesTable=newTable(stack,"Notes",{fields:{id:"string",},primaryIndex:{partitionKey:"id"},});// Create the AppSync GraphQL APIconstapi=newAppSyncApi(stack,"AppSyncApi",{schema:"packages/functions/src/graphql/schema.graphql",defaults:{function:{// Bind the table name to the functionbind:[notesTable],},},dataSources:{notes:"packages/functions/src/main.handler",},resolvers:{"Query listNotes":"notes","Query getNoteById":"notes","Mutation createNote":"notes","Mutation updateNote":"notes","Mutation deleteNote":"notes",},});// Show the AppSync API Id and API Key in the outputstack.addOutputs({ApiId:api.apiId,APiUrl:api.url,ApiKey:api.cdk.graphqlApi.apiKey||"",});}We are creating an AppSync GraphQL API here using theAppSyncApi construct. We are also creating a DynamoDB table using theTable construct. It’ll store the notes we’ll be creating with our GraphQL API.
Finally, we bind our table to our API.
Define the GraphQL schema
Add the following topackages/functions/src/graphql/schema.graphql.
typeNote{id:ID!content:String!}inputNoteInput{id:ID!content:String!}inputUpdateNoteInput{id:ID!content:String}typeQuery{listNotes:[Note]getNoteById(noteId:String!):Note}typeMutation{createNote(note:NoteInput!):NotedeleteNote(noteId:String!):StringupdateNote(note:UpdateNoteInput!):Note}Let’s also add a type for our note object.
Add the following to a new file inpackages/functions/src/graphql/Note.ts.
typeNote={id:string;content:string;};exportdefaultNote;Adding the function handler
To start with, let’s create the Lambda function that’ll be our AppSync data source.
Replacepackages/functions/src/main.ts with the following.
importNotefrom"./graphql/Note";importlistNotesfrom"./graphql/listNotes";importcreateNotefrom"./graphql/createNote";importupdateNotefrom"./graphql/updateNote";importdeleteNotefrom"./graphql/deleteNote";importgetNoteByIdfrom"./graphql/getNoteById";typeAppSyncEvent={info:{fieldName:string;};arguments:{note:Note;noteId:string;};};exportasyncfunctionhandler(event:AppSyncEvent):Promise<Record<string,unknown>[]|Note|string|null|undefined>{switch(event.info.fieldName){case"listNotes":returnawaitlistNotes();case"createNote":returnawaitcreateNote(event.arguments.note);case"updateNote":returnawaitupdateNote(event.arguments.note);case"deleteNote":returnawaitdeleteNote(event.arguments.noteId);case"getNoteById":returnawaitgetNoteById(event.arguments.noteId);default:returnnull;}}Now let’s implement our resolvers.
Create a note
Starting with the one that’ll create a note.
Add a file topackages/functions/src/graphql/createNote.ts.
import{DynamoDB}from"aws-sdk";import{Table}from"sst/node/table";importNotefrom"./Note";constdynamoDb=newDynamoDB.DocumentClient();exportdefaultasyncfunctioncreateNote(note:Note):Promise<Note>{constparams={Item:noteasRecord<string,unknown>,TableName:Table.Notes.tableName,};awaitdynamoDb.put(params).promise();returnnote;}Here, we are storing the given note in our DynamoDB table.
Let’s install theaws-sdk package in thepackages/functions/ folder package that we are using.
$npminstallaws-sdkRead the list of notes
Next, let’s write the function that’ll fetch all our notes.
Add the following topackages/functions/src/graphql/listNotes.ts.
import{DynamoDB}from"aws-sdk";import{Table}from"sst/node/table";constdynamoDb=newDynamoDB.DocumentClient();exportdefaultasyncfunctionlistNotes():Promise<Record<string,unknown>[]|undefined>{constparams={TableName:Table.Notes.tableName,};constdata=awaitdynamoDb.scan(params).promise();returndata.Items;}Here we are getting all the notes from our table.
Read a specific note
We’ll do something similar for the function that gets a single note.
Create apackages/functions/src/graphql/getNoteById.ts.
import{DynamoDB}from"aws-sdk";import{Table}from"sst/node/table";importNotefrom"./Note";constdynamoDb=newDynamoDB.DocumentClient();exportdefaultasyncfunctiongetNoteById(noteId:string):Promise<Note|undefined>{constparams={Key:{id:noteId},TableName:Table.Notes.tableName,};const{Item}=awaitdynamoDb.get(params).promise();returnItemasNote;}We are getting the note with the id that’s passed in.
Update a note
Now let’s update our notes.
Add apackages/functions/src/graphql/updateNote.ts with:
import{DynamoDB}from"aws-sdk";import{Table}from"sst/node/table";importNotefrom"./Note";constdynamoDb=newDynamoDB.DocumentClient();exportdefaultasyncfunctionupdateNote(note:Note):Promise<Note>{constparams={Key:{id:note.id},ReturnValues:"UPDATED_NEW",UpdateExpression:"SET content = :content",TableName:Table.Notes.tableName,ExpressionAttributeValues:{":content":note.content},};awaitdynamoDb.update(params).promise();returnnoteasNote;}We are using the id and the content of the note that’s passed in to update a note.
Delete a note
To complete all the operations, let’s delete the note.
Add this topackages/functions/src/graphql/deleteNote.ts.
import{DynamoDB}from"aws-sdk";import{Table}from"sst/node/table";constdynamoDb=newDynamoDB.DocumentClient();exportdefaultasyncfunctiondeleteNote(noteId:string):Promise<string>{constparams={Key:{id:noteId},TableName:Table.Notes.tableName,};// await dynamoDb.delete(params).promise();returnnoteId;}Note that, we are purposely disabling the delete query for now. We’ll come back to this later.
Let’s test what we’ve created so far!
Starting your dev environment
SST features aLive Lambda Development environment that allows you to work on your serverless apps live.
$npm run devThe first time you run this command it’ll take a couple of minutes to deploy your app and a debug stack to power the Live Lambda Development environment.
=============== Deploying app===============Preparing your SST appTranspiling sourceLinting sourceDeploying stacksdev-graphql-appsync-ExampleStack: deploying... ✅ dev-graphql-appsync-ExampleStackStack dev-graphql-appsync-ExampleStack Status: deployed Outputs: ApiId: lk2fgfxsizdstfb24c4y4dnad4 ApiKey: da2-3oknz5th4nbj5oobjz4jwid62q ApiUrl: https://2ngraxbyo5cwdpsk47wgn3oafu.appsync-api.us-east-1.amazonaws.com/graphqlTheApiId is the Id of the AppSync API we just created, theApiKey is the API key of our AppSync API andApiUrl is the AppSync API URL.
Let’s test our endpoint with theSST Console. The SST Console is a web based dashboard to manage your SST apps.Learn more about it in our docs.
Go to theGraphQL tab and you should see the GraphQL Playground in action.
Note, The GraphQL explorer lets you query GraphQL endpoints created with the GraphQLApi and AppSyncApi constructs in your app.
Let’s start by creating a note. Paste the below mutation in the left part of the playground.
mutationcreateNote{createNote(note:{id:"001",content:"My note"}){idcontent}}
Also let’s go to theDynamoDB tab in the SST Console and check that the value has been created in the table.
Note, TheDynamoDB explorer allows you to query the DynamoDB tables in theTable constructs in your app. You can scan the table, query specific keys, create and edit items.

And let’s get the note we just created by running this query instead.
querygetNoteById{getNoteById(noteId:"001"){idcontent}}
Let’s test our update mutation by running:
mutationupdateNote{updateNote(note:{id:"001",content:"My updated note"}){idcontent}}
Now let’s try deleting our note.
mutationdeleteNote{deleteNote(noteId:"001")}
Let’s test if the delete worked by getting all the notes.
querylistNotes{listNotes{idcontent}}
You’ll notice a couple of things. Firstly, the note we created is still there. This is because ourdeleteNote method isn’t actually running our query. Secondly, our note should have the updated content from our previous query.
Making changes
Let’s fix ourpackages/functions/src/graphql/deleteNote.ts by un-commenting the query.
awaitdynamoDb.delete(params).promise();If you head back to the query editor and run the delete mutation again.
mutationdeleteNote{deleteNote(noteId:"001")}
And running the list query should now show that the note has been removed!
querylistNotes{listNotes{idcontent}}
Notice we didn’t need to redeploy our app to see the change.
Deploying your API
Now that our API is tested, let’s deploy it to production. You’ll recall that we were using adev environment, the one specified in oursst.config.ts. However, we are going to deploy it to a different environment. This ensures that the next time we are developing locally, it doesn’t break the API for our users.
Run the following in your terminal.
$npx sst deploy--stage prodCleaning up
Finally, you can remove the resources created in this example using the following commands.
$npx sst remove$npx sst remove--stage prodConclusion
And that’s it! You’ve got a brand new serverless GraphQL API built with AppSync. A local development environment, to test and make changes. And it’s deployed to production as well, so you can share it with your users. Check out the repo below for the code we used in this example. And leave a comment if you have any questions!
For help and discussion
Comments on this example