DEV Community

Cover image for How to Make a Serverless GraphQL API using Lambda and DynamoDB
We're Serverless! for Serverless Inc.

Posted on • Originally published at serverless.com

4 2

How to Make a Serverless GraphQL API using Lambda and DynamoDB

Originally posted at Serverless

The graphql module makes it easy to rapidly create a GraphQL service that validates queries. We use GraphQL at Serverless.com to query our backend services, and we love how well it fits into the serverless paradigm.

Interested in building your own GraphQL API? Awesome. Here we go.

Building the API

In this example, I’ll be targeting AWS. Let’s build a simplistic version of an API that might be used by the front-end to retrieve a dynamic message to display in the UI; in this case, greeting the user by name.

Start by initializing a project and installing the graphql module:

$ npm init
$ npm install --save graphql
view raw .sh hosted with ❤ by GitHub

Now we can use it in handler.js, where we declare a schema and then use it to serve query requests:
/* handler.js */
const {
graphql,
GraphQLSchema,
GraphQLObjectType,
GraphQLString,
GraphQLNonNull
} = require('graphql')
// This method just inserts the user's first name into the greeting message.
const getGreeting = firstName => `Hello, ${firstName}.`
// Here we declare the schema and resolvers for the query
const schema = new GraphQLSchema({
query: new GraphQLObjectType({
name: 'RootQueryType', // an arbitrary name
fields: {
// the query has a field called 'greeting'
greeting: {
// we need to know the user's name to greet them
args: { firstName: { name: 'firstName', type: new GraphQLNonNull(GraphQLString) } },
// the greeting message is a string
type: GraphQLString,
// resolve to a greeting message
resolve: (parent, args) => getGreeting(args.firstName)
}
}
}),
})
// We want to make a GET request with ?query=<graphql query>
// The event properties are specific to AWS. Other providers will differ.
module.exports.query = (event, context, callback) => graphql(schema, event.queryStringParameters.query)
.then(
result => callback(null, {statusCode: 200, body: JSON.stringify(result)}),
err => callback(err)
)
view raw handler.js hosted with ❤ by GitHub

Pretty simple! To deploy it, define a service in serverless.yml, and set the handler to service HTTP requests:
# serverless.yml
service: graphql-api
functions:
query:
handler: handler.query
events:
- http:
path: query
method: get
view raw serverless.yml hosted with ❤ by GitHub

Now we can bring it to life:
$ serverless deploy
# Serverless: Packaging service...
# Serverless: Excluding development dependencies...
# Serverless: Uploading CloudFormation file to S3...
# Serverless: Uploading artifacts...
# Serverless: Uploading service .zip file to S3 (357.34 KB)...
# Serverless: Validating template...
# Serverless: Updating Stack...
# Serverless: Checking Stack update progress...
# ..............
# Serverless: Stack update finished...
# Service Information
# service: graphql-api
# stage: dev
# region: us-east-1
# stack: graphql-api-dev
# api keys:
# None
# endpoints:
# GET - https://9qdmq5nvql.execute-api.us-east-1.amazonaws.com/dev/query
# functions:
# query: graphql-api-dev-query
$ curl -G 'https://9qdmq5nvql.execute-api.us-east-1.amazonaws.com/dev/query' --data-urlencode 'query={greeting(firstName: "Jeremy")}'
# {"data":{"greeting":"Hello, Jeremy."}}
view raw serverless.sh hosted with ❤ by GitHub

Creating the database

In the real world, virtually any service that does something valuable has a data store behind it. Suppose users have nicknames that should appear in the greeting message; we need a database to store those nicknames, and we can expand our GraphQL API to update them.

Let’s start by adding a database to the resource definitions in serverless.yml. We need a table keyed on the user's first name, which we define using CloudFormation, as well as some provider configuration to allow our function to access it:

# add to serverless.yml
provider:
name: aws
runtime: nodejs6.10
environment:
DYNAMODB_TABLE: ${self:service}-${self:provider.stage}
iamRoleStatements:
- Effect: Allow
Action:
- dynamodb:GetItem
- dynamodb:UpdateItem
Resource: "arn:aws:dynamodb:${opt:region, self:provider.region}:*:table/${self:provider.environment.DYNAMODB_TABLE}"
resources:
Resources:
NicknamesTable:
Type: 'AWS::DynamoDB::Table'
Properties:
AttributeDefinitions:
- AttributeName: firstName
AttributeType: S
KeySchema:
- AttributeName: firstName
KeyType: HASH
ProvisionedThroughput:
ReadCapacityUnits: 1
WriteCapacityUnits: 1
TableName: ${self:provider.environment.DYNAMODB_TABLE}
view raw serverless.yml hosted with ❤ by GitHub

To use it, we’ll need the aws-sdk. Here’s how you’d use the SDK’s vanilla DocumentClient to access DynamoDB records:
$ npm install --save aws-sdk
view raw .sh hosted with ❤ by GitHub

Include these in our handler, and then we can get to work:
// add to handler.js
const AWS = require('aws-sdk');
const dynamoDb = new AWS.DynamoDB.DocumentClient();
view raw handler.js hosted with ❤ by GitHub

We started by defining a method that returned a simple string value for the greeting message. However, the GraphQL library can also use Promises as resolvers.

Since the DocumentClient uses a callback pattern, we’ll wrap these in promises and use the DynamoDB get method to check the database for a nickname for the user:

// add to handler.js
const promisify = foo => new Promise((resolve, reject) => {
foo((error, result) => {
if(error) {
reject(error)
} else {
resolve(result)
}
})
})
// replace previous implementation of getGreeting
const getGreeting = firstName => promisify(callback =>
dynamoDb.get({
TableName: process.env.DYNAMODB_TABLE,
Key: { firstName },
}, callback))
.then(result => {
if(!result.Item) {
return firstName
}
return result.Item.nickname
})
.then(name => `Hello, ${name}.`)
// add method for updates
const changeNickname = (firstName, nickname) => promisify(callback =>
dynamoDb.update({
TableName: process.env.DYNAMODB_TABLE,
Key: { firstName },
UpdateExpression: 'SET nickname = :nickname',
ExpressionAttributeValues: {
':nickname': nickname
}
}, callback))
.then(() => nickname)
view raw handler.js hosted with ❤ by GitHub

You can see here that we added a method changeNickname, but the GraphQL API is not yet using it. We need to declare a mutation that the front-end can use to perform updates.

We previously only added a query declaration to the schema; now we need a mutation as well:

// alter schema
const schema = new GraphQLSchema({
query: new GraphQLObjectType({
/* unchanged */
}),
mutation: new GraphQLObjectType({
name: 'RootMutationType', // an arbitrary name
fields: {
changeNickname: {
args: {
// we need the user's first name as well as a preferred nickname
firstName: { name: 'firstName', type: new GraphQLNonNull(GraphQLString) },
nickname: { name: 'nickname', type: new GraphQLNonNull(GraphQLString) }
},
type: GraphQLString,
// update the nickname
resolve: (parent, args) => changeNickname(args.firstName, args.nickname)
}
}
})
})
view raw .js hosted with ❤ by GitHub

After these changes, we can make the greeting request again and receive the same result as before:
$ curl -G 'https://9qdmq5nvql.execute-api.us-east-1.amazonaws.com/dev/query' --data-urlencode 'query={greeting(firstName: "Jeremy")}'
# {"data":{"greeting":"Hello, Jeremy."}}
view raw .sh hosted with ❤ by GitHub

But if I want the API to call me “Jer”, I can update the nickname for “Jeremy”:
$ curl -G 'https://9qdmq5nvql.execute-api.us-east-1.amazonaws.com/dev/query' --data-urlencode 'query=mutation {changeNickname(firstName:
"Jeremy", nickname: "Jer")}'
$ curl -G 'https://9qdmq5nvql.execute-api.us-east-1.amazonaws.com/dev/query' --data-urlencode 'query={greeting(firstName: "Jeremy")}'
# {"data":{"greeting":"Hello, Jer."}}
view raw .sh hosted with ❤ by GitHub

The API will now call anyone named “Jeremy” by the nickname “Jer”.

Separation of concerns like this let you build front-ends and services that offload logic into backends. Those backends can then encapsulate data access and processing behind a strongly-typed, validating, uniform contract that comes with rich versioning and deprecation strategies.

Deploy your own!

To deploy this service yourself, download the source code and deploy it with the Serverless Framework. Or, take a look at a larger example project for ideas on project structure and factoring.

Architectural Diagram

Happy building!

Originally published at https://www.serverless.com.

Heroku

Build apps, not infrastructure.

Dealing with servers, hardware, and infrastructure can take up your valuable time. Discover the benefits of Heroku, the PaaS of choice for developers since 2007.

Visit Site

Top comments (0)

Billboard image

The Next Generation Developer Platform

Coherence is the first Platform-as-a-Service you can control. Unlike "black-box" platforms that are opinionated about the infra you can deploy, Coherence is powered by CNC, the open-source IaC framework, which offers limitless customization.

Learn more

👋 Kindness is contagious

Please leave a ❤️ or a friendly comment on this post if you found it helpful!

Okay