Servlerless GraphQL API

By Marcin Piczkowski Comment
Subscribe

We’re going to study how to develop Serverless API with GraphQL.

GraphQL is a query language developed by Facebook. The API using GraphQL is developed in such a way that one endpoint can serve many resources, in contrary to REST style APIs which implement endpoints per resource. By query definition we can decide which resources we want to pull from back-end services. What is also important is that the documentation is the first-class citizen of GraphQL. There is also a nice tool called GraphiQL which is an in-browser IDE for exploring GraphQL schema.

Developing GraphQL function

Used node modules

We’re going to use the following NodeJS modules:

  • bluebird - for Promises construction
  • graphql - for GraphQL schema support
  • stampit - for easy JS objects construction
  • istanbul - for code coverage with tests
  • snyk - for vulnerabilities scan in our code and dependencies
  • mocha - for unit tests

There are also some other modules used but the above are the most important.

The code for this post can be found on Github

This is a continuation of previous post in which we added first “Hello” function. Here we’re going to add a new function which will handle all GraphQL requests. We will add a new object type in our schema called Item which will be an item ready for reservation.

Two operations on Item will be supported: listing available items by name and creating new items.

At the moment we do not yet have persistence in the application so the operations will just return hard-coded data.

Let’s begin with adding new handler function. In serverless.yml we have to add:

functions:
  graphQl:
    handler: index.graphql
    memory: 512
    timeout: 3
    events:
      - http:
          path: graphql
          method: post

We will call our handler graphQl but any name would we completely fine. The handler is implemented in file index.js which we’re going to create in the root folder with the following content:

const itemService = require('./src/services/item');
const graphQLService = require('./src/services/graphql');

const serviceInstance = itemService();
const graphql = graphQLService({ itemService: serviceInstance });

module.exports.graphql = (event, context, cb) => {
    console.log('Received event', event);

    return graphql.runGraphQL(event.body)
            .then((response) => {
                console.log("Response: ",JSON.stringify(response));
                const newResponse = {
                    statusCode: 200,
                    body: JSON.stringify(response)
                };
                cb(null, newResponse)})
            .catch((err) => {
                console.log("Error: ", err);

                var myErrorObj = {
                    errorType : "InternalServerError",
                    httpStatus : 500,
                    requestId : context.awsRequestId,
                    message : "An unknown error has occurred. Please try again."
                };
                cb(JSON.stringify(myErrorObj))
            });
};

It imports two services:

  • itemService from file src/services/item.js This service will contain methods for Item such as listing and creating.
  • graphQLService from file src/services/graphql.js This service will handle all requests using graphql-js library. The result of graphql.runGraphQL method is a Promise, then we handle success and error separately. It was tempting to return response of this handler as JSON object like this:
    return graphql.runGraphQL(event.body)
        .then((response) => cb(null, response))
        .catch((err) => cb(err));

Unfortunately it did not work. Looks like CloudFront which stand before AWS Gateway in AWS infrastructure does not understand such response and expects each one to have structure like:

{
  statusCode: 200,
  body: JSON.stringify(response)
}

and this has to be formatted as a string, not JSON object, therefore we invoke JSON.stringify

I was looking for justification of this but could not find. Also error handling part did not work for me and I always received HTTP status 502 Bad Gateway, even though Amazon documentation states that is should work.

Anyway, going further, the graphQLService will need schema definition which we’re going to add in src/schema.js

The schema will look as follows:

const graphql = require('graphql');

module.exports = (itemService) => {

    const Date = new graphql.GraphQLScalarType({
        name: 'Date',
        description: 'Date custom scalar type',
        parseValue(value) {
            return new Date(value); // value from the client
        },
        serialize(value) {
            return value.getTime(); // value sent to the client
        },
        parseLiteral(ast) {
            if (ast.kind === graphql.Kind.INT) {
                return parseInt(ast.value, 10); // ast value is always in string format
            }
            return null;
        },
    });

    const ItemType = new graphql.GraphQLObjectType({
        name: 'Item',
        fields: {
            name: {type: graphql.GraphQLString},
            description: {type: graphql.GraphQLString},
            createdAt: {type: Date}
        },
    });

    const Query = new graphql.GraphQLObjectType({
        name: 'Root',
        description: 'Root of the Schema',
        fields: () => ( {
            items: {
                name: 'ItemQuery',
                description: 'Retrieve items',
                type: new graphql.GraphQLList(ItemType),
                args: {
                    name: {
                        type: graphql.GraphQLString,
                    }
                },
                resolve(_, args, ast) { // eslint-disable-line no-unused-vars
                    return itemService.findByName(args.name);
                }
            }
        })
    });

    const Mutuation = new graphql.GraphQLObjectType({
        name: 'ItemMutations',
        fields: () => ( {
            createItem: {
                type: ItemType,
                description: "Create item for reservation",
                args: {
                    id: {type: new graphql.GraphQLNonNull(graphql.GraphQLString)},
                    name: {type: graphql.GraphQLString},
                    description: {type: graphql.GraphQLString}
                },
                resolve: function (source, args) {
                    return itemService.createItem(args);
                }
            }
        })
    });

    const schema = new graphql.GraphQLSchema({
        query: Query,
        mutation: Mutuation
    });

    return schema;
};

There we defined also new type Date as GraphQL does not contain such data type out of the box. We also defined Item object.

The schema definition module takes as input argument itemService which is used to query and create Items. In future if we wanted to add other object types we would create a separate service for each type and add it as an argument in the module. For now we keep schema in single file for simplicity. It is easier to view the whole context, but this may become a bit messy when the schema grows and we have to add more services. Then we will probably to cut it in chunks and split into separate schema files.

I will not explain the code of services. It can be found on Github, but there is one interesting thing to mention about them. We defined services using node library called stampit

It allows to define objects in a nice, composable way using fluent API. Its purpose is also to standarise the way Object Oriented programming is used, as the language gives many ways of using it. What authors also say about it is that

Stampit uses three different kinds of prototypal OO to let you inherit behavior in a way that is much more powerful and flexible than classical OO.

E.g we can use a kind of template for separating constructors, fields and methods

const GraphQLService = stampit()
    .init((opts) => {
        if (!opts.instance.itemService) throw new Error('itemService is required');

        const schema = schemaFactory(opts.instance.itemService);
        opts.instance.schema = schema; // eslint-disable-line no-param-reassign
    })
    .methods({
        runGraphQL(query) {
            return graphql.graphql(this.schema, query);
        },
    })
    .compose(Logger);

There is a nice talk about Object Oriented programming practices and stampit library.

Testing GraphQL function locally manually

Run command to insert new item

serverless invoke local -f graphQl -p tests/test_create_item.json

It returns:

{
    "data": {
        "createItem": {
            "name": "demo item",
            "createdAt": 1489281413395
        }
    }
}

Run command to list items

serverless invoke local -f graphQl -p tests/test_query_items.json

It returns:

{
    "data": {
        "items": [
            {
                "name": "item name",
                "createdAt": 1489281436932
            }
        ]
    }
}

We can then deploy the function and test on AWS similarly:

serverless deploy
serverless invoke -f graphQl -p tests/test_create_item.json
serverless logs -f graphQl -t

The last command will print server logs.

How would we test the endpoint without Serverless framework? Nothing more simple - we just need any HTTP client. Curl would be sufficient. We can then execute from command line:

curl -X POST \
-H "Content-Type: application/json" \
-d '{items(name: "demo item") {name, createdAt}}' \
<endpoint URL>

In the above command we need to replace <endpoint URL> with the real URL of API Gateway which should be printed out from the serverless deploy command.

Automatic tests

We used Mocha and Snyk to develop automatic tests. Mocha is a well known testing library for JS. Snyk is a library used to discover code vulnerabilities.

To use Snyk you need to first setup a free account at https://snyk.io. You can sign up with your github account. Then install Snyk globally

 npm i -g snyk

Authenticate in Snyk

snyk auth

Now you can run unit tests with

npm test

The target “test” is configured in package.json as:

...
"scripts": {
  "lint": "eslint .",
  "test": "snyk test && npm run test:unit",
  "test:unit": "istanbul cover _mocha tests/all -- -R spec --recursive"
},
...

It will run unit tests thanks to Mocha and gather test coverage with Istanbul.

In the next blog post we’re going to secure our API Gateway endpoint and add persistence in Dynamo DB.

References

comments powered by Disqus