Build a GraphQL API with the Serverless framework

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:

  • Strong typing. With GraphQL, all fields in the request and response need to conform to a previously declared type. This helps prevent a large class of bugs.
  • Client-specified interfaces. The client specifies the fields they wish to retrieve (or update) in the request and the server returns only those fields.
  • Retrieving multiple levels of data in a query. With GraphQL, it’s much easier to ask for a user, along with all posts belonging to that user, as well as their comments, in a single query.

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:

  • Adding a product to the warehouse.
  • Retrieving all products in the warehouse.
  • Retrieving a single product.
  • Removing a product from the warehouse.

We’ll use AWS DynamoDB as our data store. Let’s go!

Prerequisites

  1. Node.js v6.10 or later
  2. An AWS account. You can sign up for a free account here.

Setting up the project

First, install the Serverless CLI:

npm 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:

  serverless 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:

npm init -y

Then install dependencies by running:

npm install graphql aws-sdk

Configuring our service

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:

 service: stockup-gql

    provider:
      name: aws
      runtime: nodejs6.10
      iamRoleStatements:
              - Effect: Allow
                Action:
                  - dynamodb:DescribeTable
                  - dynamodb:Query
                  - dynamodb:Scan
                  - dynamodb:GetItem
                  - dynamodb:PutItem
                  - dynamodb:UpdateItem
                  - dynamodb:DeleteItem
                Resource:
                   Fn::Join:
                     - ""
                     - - "arn:aws:dynamodb:*:*:table/"
                       - Ref: ProductsGqlDynamoDbTable

    functions:
      queryProducts:
        handler: handler.queryProducts
        events:
        - http:
            path: products
            method: post
            cors: true
        environment:
          TABLE_NAME: products-gql

    resources:
      Resources:
        ProductsGqlDynamoDbTable:
          Type: AWS::DynamoDB::Table
          Properties:
            TableName: products-gql
            AttributeDefinitions:
              - AttributeName: id
                AttributeType: S
            KeySchema:
              - AttributeName: id
                KeyType: HASH
            ProvisionedThroughput:
              ReadCapacityUnits: 1
              WriteCapacityUnits: 1

A brief explanation of this file:

  • The service key contains the name of our service (“stockup-gql”)
  • The 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:
    • The runtime environment that our service will run in (Node.js)
    • The IAM (Identity Access Management) role that our functions will run under. Our functions need to read from and write to our DynamoDB permissions, so we’ve added the necessary permissions to the IAM role.
  • The 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).
  • The 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.

Writing our application logic

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:

'use strict';

    const AWS = require('aws-sdk');
    const dynamoDb = new AWS.DynamoDB.DocumentClient();
    const uuid = require('uuid');

    module.exports = (data) => {
        const params = {
            TableName: process.env.TABLE_NAME,
            Item: {
                name: data.name,
                quantity: data.quantity,
                id: uuid.v1(),
                addedAt: Date.now(),
            }
        };
        return dynamoDb.put(params).promise()
            .then(result => params.Item)
    };

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:

'use strict';

    const AWS = require('aws-sdk');
    const dynamoDb = new AWS.DynamoDB.DocumentClient();

    module.exports = () => dynamoDb.scan({ TableName: process.env.TABLE_NAME })
        .promise()
        .then(r => r.Items);

Our view function (resolvers/view.js) takes in the product ID and returns the corresponding product using dynamoDb.get:

'use strict';

    const AWS = require('aws-sdk');
    const dynamoDb = new AWS.DynamoDB.DocumentClient();

    module.exports = (id) => {
        const params = {
            TableName: process.env.TABLE_NAME,
            Key: { id }
        };
        return dynamoDb.get(params).promise()
            .then(r => r.Item);
    };

And our remove function (resolvers/remove.js) also takes a product ID, then uses the delete command to remove the corresponding product:

'use strict';

    const AWS = require('aws-sdk');
    const dynamoDb = new AWS.DynamoDB.DocumentClient();

    module.exports = (id) => {
        const params = {
            TableName: process.env.TABLE_NAME,
            Key: { id }
        };
        return dynamoDb.delete(params).promise()
    };

All good.

Defining our schema

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:

'use strict';

    const {
        GraphQLSchema,
        GraphQLObjectType,
        GraphQLString,
        GraphQLInt,
        GraphQLList,
        GraphQLNonNull,
        GraphQLBoolean
    } = require('graphql');
    const addProduct = require('./resolvers/create');
    const viewProduct = require('./resolvers/view');
    const listProducts = require('./resolvers/list');
    const removeProduct = require('./resolvers/remove');

    const productType = new GraphQLObjectType({
        name: 'Product',
        fields: {
            id: { type: new GraphQLNonNull(GraphQLString) },
            name: { type: new GraphQLNonNull(GraphQLString) },
            quantity: { type: new GraphQLNonNull(GraphQLInt) },
            addedAt: { type: new GraphQLNonNull(GraphQLString) },
        }
    });


    const schema = new GraphQLSchema({
        query: new GraphQLObjectType({
            name: 'Query',
            fields: {
                listProducts: {
                    type: new GraphQLList(productType),
                    resolve: (parent, args) => listProducts()
                },
                viewProduct: {
                    args: {
                        id: { type: new GraphQLNonNull(GraphQLString) }
                    },
                    type: productType,
                    resolve: (parent, args) => viewProduct(args.id)
                }
            }
        }),

        mutation: new GraphQLObjectType({
            name: 'Mutation',
            fields: {
                createProduct: {
                    args: {
                        name: { type: new GraphQLNonNull(GraphQLString) },
                        quantity: { type: new GraphQLNonNull(GraphQLInt) }
                    },
                    type: productType,
                    resolve: (parent, args) => addProduct(args)
                },
                removeProduct: {
                    args: {
                        id: { type: new GraphQLNonNull(GraphQLString) }
                    },
                    type: GraphQLBoolean,
                    resolve: (parent, args) => removeProduct(args.id)
                },
            }
        })
    });

    module.exports = schema;

Here’s an explanation of the code in this file:

  • The first thing we define is a 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.
  • Next, we define our schema. GraphQL supports two kinds of operations: queries and mutations. Queries are used for fetching data, while mutations are used for making changes to data (for instance, creating or removing a product), These operations are also defined as types in the query and mutation fields of our schema object. The fieldvalues 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.

Bringing it all together

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:

'use strict';

    const { graphql } = require('graphql');
    const schema = require('./schema');

    module.exports.queryProducts = (event, context, callback) => {
        graphql(schema, event.body)
            .then(result => callback(null, {statusCode: 200, body: JSON.stringify(result)}))
            .catch(callback);
    };

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.

Deploying and querying our API

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:

 serverless deploy

When the command is done, you should see output like this:

Service Information
    service: stockup-gql
    stage: dev
    region: us-east-1
    stack: stockup-gql-dev
    api keys:
      None
    endpoints:
      POST - https://xxxxxx.execute-api.us-east-1.amazonaws.com/dev/products
    functions:
      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:

mutation {
      createProduct (name: "Productine", quantity: 2) {
        id,
        name,
        quantity,
        addedAt
      }
    }

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.

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):

query {
      viewProduct (id: "<id>") {
        name,
        addedAt
      }
    }

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.

Similarly, we can retrieve all products with this query:

query {
      listProducts {
        name,
        addedAt
      }
    }

And remove the product we just created using this:

mutation {
      removeProduct (id: "<id>")
    }

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.

Conclusion

Here are a few more resources for further research:

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.

You May Also Like
Read More

Froxt DevDay

It’s almost time! Froxt DevDay – The Meetup, a fresh take on our yearly virtual developer event, begins…
Read More

Introducing Froxt DCP

In 2021, we started an initiative called Froxt Work Management (FWM) Cloud First Releases for Ecosystem which enabled…