In this tutorial, we will build a GraphQL API, and deploy it using the Serverless framework for AWS. Use Node.js for the backend, along with AWS' Lambda functions.
In a previous article, we explored how to build a serverless REST API using AWS Lambda and the Serverless framework. In this article, we’ll build a different version of that API, providing the same functionality, but through a GraphQL interface instead.
GraphQL is a query language for interacting with APIs. It provides a lot of benefits such as:
With GraphQL, all requests are made to one endpoint. The data to return or operation to perform is determined by the query
specified in the request.
Here’s a brief overview of the operations our service will support via GraphQL queries:
We’ll use AWS DynamoDB as our data store. Let’s go!
First, install the Serverless CLI:
1npm install -g serverless
Next, we’ll create a new service using the AWS Node.js template. Create a folder to hold your service (I’m calling mine stockup-gql
) and run the following command in it:
1serverless create --template aws-nodejs
This will populate the current directory with the starter files needed for the service.
Let’s add our application dependencies before we go on. Run the following command to set up a package.json
file in your application directory:
1npm init -y
Then install dependencies by running:
1npm install graphql aws-sdk
The serverless.yml
file acts as a manifest for our service. It contains information that the Serverless CLI will use to configure and deploy our service to AWS. Replace the contents of your serverless.yml
file with the following:
1service: stockup-gql 2 3 provider: 4 name: aws 5 runtime: nodejs6.10 6 iamRoleStatements: 7 - Effect: Allow 8 Action: 9 - dynamodb:DescribeTable 10 - dynamodb:Query 11 - dynamodb:Scan 12 - dynamodb:GetItem 13 - dynamodb:PutItem 14 - dynamodb:UpdateItem 15 - dynamodb:DeleteItem 16 Resource: 17 Fn::Join: 18 - "" 19 - - "arn:aws:dynamodb:*:*:table/" 20 - Ref: ProductsGqlDynamoDbTable 21 22 functions: 23 queryProducts: 24 handler: handler.queryProducts 25 events: 26 - http: 27 path: products 28 method: post 29 cors: true 30 environment: 31 TABLE_NAME: products-gql 32 33 resources: 34 Resources: 35 ProductsGqlDynamoDbTable: 36 Type: AWS::DynamoDB::Table 37 Properties: 38 TableName: products-gql 39 AttributeDefinitions: 40 - AttributeName: id 41 AttributeType: S 42 KeySchema: 43 - AttributeName: id 44 KeyType: HASH 45 ProvisionedThroughput: 46 ReadCapacityUnits: 1 47 WriteCapacityUnits: 1
A brief explanation of this file:
service
key contains the name of our service (“stockup-gql”)provider
key is where we specify the name of the provider we’re using (AWS) and configurations specific to it. Here, we’ve specified two configurations:
runtime
environment that our service will run in (Node.js)functions
key holds the functions provided by our service, the events (API calls) that should trigger them, and their handlers (we’ll write the code for the handlers soon). We have just one function, the GraphQL endpoint we’ve called queryProducts
. For this function, we specify the events that should trigger it (a HTTP request) as well as an environment variable to pass to it (the database table name).resources
key contains all necessary configuration for AWS resources our service will access. In our case, we’ve configured the DynamoDB resource by specifying the name of the table we’ll be interacting with (products). DynamoDB is schemaless but requires you to declare the primary key for each table, so we’ve defined this in our AttributeDefinitions
and KeySchema
. We’re using the id
, a string, as our primary key.First, let’s write the code that interacts with our database directly. Create a directory called resolvers
. We’ll export these functions and provide them to GraphQL for handling the query.
Create a file called create.js
in the resolvers
directory with the following code:
1'use strict'; 2 3 const AWS = require('aws-sdk'); 4 const dynamoDb = new AWS.DynamoDB.DocumentClient(); 5 const uuid = require('uuid'); 6 7 module.exports = (data) => { 8 const params = { 9 TableName: process.env.TABLE_NAME, 10 Item: { 11 name: data.name, 12 quantity: data.quantity, 13 id: uuid.v1(), 14 addedAt: Date.now(), 15 } 16 }; 17 return dynamoDb.put(params).promise() 18 .then(result => params.Item) 19 };
In this file, we’re exporting a function that takes in the product data (sent by the user in the body of the request). Our function then creates a new product in the database, returning the result via a Promise
.
Next up is our list function (resolvers/list.js
). We don’t need any parameters for this. We call the DynamoDB scan
command to get all the products:
1'use strict'; 2 3 const AWS = require('aws-sdk'); 4 const dynamoDb = new AWS.DynamoDB.DocumentClient(); 5 6 module.exports = () => dynamoDb.scan({ TableName: process.env.TABLE_NAME }) 7 .promise() 8 .then(r => r.Items);
Our view function (resolvers/view.js
) takes in the product ID and returns the corresponding product using dynamoDb.get
:
1'use strict'; 2 3 const AWS = require('aws-sdk'); 4 const dynamoDb = new AWS.DynamoDB.DocumentClient(); 5 6 module.exports = (id) => { 7 const params = { 8 TableName: process.env.TABLE_NAME, 9 Key: { id } 10 }; 11 return dynamoDb.get(params).promise() 12 .then(r => r.Item); 13 };
And our remove function (resolvers/remove.js
) also takes a product ID, then uses the delete
command to remove the corresponding product:
1'use strict'; 2 3 const AWS = require('aws-sdk'); 4 const dynamoDb = new AWS.DynamoDB.DocumentClient(); 5 6 module.exports = (id) => { 7 const params = { 8 TableName: process.env.TABLE_NAME, 9 Key: { id } 10 }; 11 return dynamoDb.delete(params).promise() 12 };
All good.
GraphQL is a strongly typed query language. This means we have to define our schema beforehand. Our schema will specify the possible operations that can be performed on our data, as well as type definitions for our data. Our schema will also map resolvers (the functions we wrote in the last section) to these operations, allowing GraphQL to build a response to a query.
Let’s write our schema now. Create a file called schema.js
in your project’s root directory with the following content:
1'use strict'; 2 3 const { 4 GraphQLSchema, 5 GraphQLObjectType, 6 GraphQLString, 7 GraphQLInt, 8 GraphQLList, 9 GraphQLNonNull, 10 GraphQLBoolean 11 } = require('graphql'); 12 const addProduct = require('./resolvers/create'); 13 const viewProduct = require('./resolvers/view'); 14 const listProducts = require('./resolvers/list'); 15 const removeProduct = require('./resolvers/remove'); 16 17 const productType = new GraphQLObjectType({ 18 name: 'Product', 19 fields: { 20 id: { type: new GraphQLNonNull(GraphQLString) }, 21 name: { type: new GraphQLNonNull(GraphQLString) }, 22 quantity: { type: new GraphQLNonNull(GraphQLInt) }, 23 addedAt: { type: new GraphQLNonNull(GraphQLString) }, 24 } 25 }); 26 27 28 const schema = new GraphQLSchema({ 29 query: new GraphQLObjectType({ 30 name: 'Query', 31 fields: { 32 listProducts: { 33 type: new GraphQLList(productType), 34 resolve: (parent, args) => listProducts() 35 }, 36 viewProduct: { 37 args: { 38 id: { type: new GraphQLNonNull(GraphQLString) } 39 }, 40 type: productType, 41 resolve: (parent, args) => viewProduct(args.id) 42 } 43 } 44 }), 45 46 mutation: new GraphQLObjectType({ 47 name: 'Mutation', 48 fields: { 49 createProduct: { 50 args: { 51 name: { type: new GraphQLNonNull(GraphQLString) }, 52 quantity: { type: new GraphQLNonNull(GraphQLInt) } 53 }, 54 type: productType, 55 resolve: (parent, args) => addProduct(args) 56 }, 57 removeProduct: { 58 args: { 59 id: { type: new GraphQLNonNull(GraphQLString) } 60 }, 61 type: GraphQLBoolean, 62 resolve: (parent, args) => removeProduct(args.id) 63 }, 64 } 65 }) 66 }); 67 68 module.exports = schema;
Here’s an explanation of the code in this file:
Product
type. This type represents a single product in our database. For each product, we’ll store the name, the quantity, a timestamp marking when it was added, and a unique ID. We’ll need this type when constructing our schema.query
and mutation
fields of our schema
object. The field
values of the query
and mutation
objects contain the queries and mutations we’re supporting, and we call our resolvers in the resolve
function in order to obtain the result.Now we need to update our handler.js
to pass the input request to GraphQL and return the result. This is actually pretty easy to do. Replace the code in your handler.js
with the following:
1'use strict'; 2 3 const { graphql } = require('graphql'); 4 const schema = require('./schema'); 5 6 module.exports.queryProducts = (event, context, callback) => { 7 graphql(schema, event.body) 8 .then(result => callback(null, {statusCode: 200, body: JSON.stringify(result)})) 9 .catch(callback); 10 };
The first argument we pass to the graphql
function is the schema we’ve built. This tells GraphQL what to validate against and how to resolve the request into our application logic. The second parameter is the request which we are receiving as the body of the POST request.
Note: you’ll need to first configure the Serverless CLI with your AWS credentials. Serverless has published a guide on that (in video and text formats).
Run this command to deploy your service to AWS:
1serverless deploy
When the command is done, you should see output like this:
1Service Information 2 service: stockup-gql 3 stage: dev 4 region: us-east-1 5 stack: stockup-gql-dev 6 api keys: 7 None 8 endpoints: 9 POST - https://xxxxxx.execute-api.us-east-1.amazonaws.com/dev/products 10 functions: 11 queryProducts: stockup-gql-dev-queryProducts
Copy the URL shown under the endpoints
section. This is where we’ll send our API queries.
Now let’s test the API. Open up Postman or whatever API testing tool you use. First, we’ll try to create a new product. Make a POST request to the API URL with the body contents as follows:
1mutation { 2 createProduct (name: "Productine", quantity: 2) { 3 id, 4 name, 5 quantity, 6 addedAt 7 } 8 }
Here we’re running the createProduct
mutation, providing a name and quantity as we required in the args
field of the mutation in our schema. The attributes listed in braces represent the fields we wish to see when the result is returned to us.
When you run the request, you should get a response like this:
Let’s try viewing the product we just created. Change the contents of your request to the following (replace <id>
with the ID of the product you just created):
1query { 2 viewProduct (id: "<id>") { 3 name, 4 addedAt 5 } 6 }
Note that now we’re only asking for the name
and addedAt
fields in the response, so those are the only fields that will be present in it. Here’s what your response should look like when you run this:
Similarly, we can retrieve all products with this query:
1query { 2 listProducts { 3 name, 4 addedAt 5 } 6 }
And remove the product we just created using this:
1mutation { 2 removeProduct (id: "<id>") 3 }
Note that in our schema we defined this mutation to return a boolean value, so we can’t request for any fields on the response.
Play around with your API and watch what happens when you omit some required arguments (such as not passing an ID to viewProduct
) or request a nonexistent field in the response, or try to perform a nonexistent query.
In this article, we’ve built a GraphQL API hosted on AWS Lambda. We’ve seen how GraphQL helps us provide a consistent, type-safe and powerful interface to our API, and automatically validate all incoming requests. In a large app, these are very useful features to ensure consistent performance.
Here are a few more resources for further research:
You can also check out the full source code of our app on GitHub.