Building a serverless GraphQL API with Node.js, AWS Lambda and Apollo

thomas maximini
6 min readDec 10, 2019

--

Why serverless?

What are the advantages of a serverless api?

  1. Pay as you go: We have zero initial costs and pay only for what we actually use. So while we develop our app, we have close to zero costs, and depending on how it develops it will scale automatically when traffic increases.
  2. Infinite Scaling: We don’t have to deal with infrastructure or servers, so when our requests suddenly goes from 0 to 1000s of requests per second, it won’t be a problem because the serverless api will scale automatically for us. (AWS handles that in the background)
  3. Faster development: We don’t have to deal with managing , installing and configuring servers, or with deployment processes, so we can just focus on the business logic and building our app.

Serverless might not be the answer for all our problems but an GraphQL server seems like a great use case, so let’s dive right in.

Architecture

There are multiple providers for serverless functions, but the biggest and most famous one is AWS Lambda. This is the one we will be using here. Other famous examples providers are Azure Functions, Google Cloud Functions and Cloudflare Workers.

To make our lives easier we are going to leverage the serverless framework for automating our deploys, so we can just define our deployment configuration in a serverless.yml file and don't need to worry about manual deployment. Therefore we should install the serverless cli globally using npm install -g serverless. This installs a global cli tool which can be invoked by serverless or sls.

As a GraphQL server we are using Apollo, which is also one of the most famous GraphQL clients and servers. Apollo Client now fully supports React Hooks which is one of the reasons I love using it. The server-side implementation with Apollo is also very straightforward and they offer an AWS Lambda compatible package, apollo-server-lambda, which we are going to use in this tutorial.

Pre-Requisites

You should have an AWS Account and the AWS cli installed. You should have run aws configure and setup your local machine with the AWS Access Key ID and AWS Secret Access Key from an IAM user with privileges to access DynamoDB.

The Apollo Server

GraphQL comes with a default UI, the grahiQL client, which gives us the possibility to inspect a schema and to request data from an API. Apollo Server also mocks our schema by default, so the initial setup of our GraphQL server is very compact — there is not much code needed at all to get started.

Assuming we are in a fresh project folder.

First, let’s do an

I install both apollo-server and apollo-server-lambda here, because I want to develop most of the time locally and deploy to lambda every once in a while. Both the local and the lambda versions are going to use the same schema, so let’s create a file grapqhl/schema.js from which we can then export our schema.

This defines a fairly simple schema for Product which might resemble some form of e-commerce article data.

For quicker local development, we create a file local.js, where we import ApolloServer and our schema and fire up a local instance.

For local development we will npm install --save-dev nodemon and add a npm script to our package.json:

This will automatically restart the apollo-server in local.js whenever we change any files.

For the serverless function we create a file lambda.js with the following content:

This one is very similar to the local.js development server, except that we import ApolloServer from the apollo-server-lambda package now and we add some extra context object to the constructor (more on that later). We just export the the ApolloServer instance here again.

Now create another file, index.js, where we import said server.

Now create the serverless.yml file.

What is going on here? Serverless framework is configured using a yaml file — the most important point here are:

  • we want to use AWS as a provider (Lambda) with a nodejs runtime of version 10.x
  • we want to run it in eu-central-01 region (You might want to change that depending on your location)
  • we need some IAM policies
  • most importantly, we we tell serverless which functions should correspond to which endpoints: Here we have a function (we call it graphql) that uses the handler index.graphqlHandler, this means go check index.js for a function called graphqlHandler and then use this for the following event definitions, in our case a simple http get with a path of /graphql.

The first test

Let’s run npm run dev and the local development server should start up:

🚀 Server ready at http://localhost:4000/

Visit http://localhost:4000 and confirm everything works, you should see a GraphQL Playground.

As ApolloServer mocks everything by default, you can now run your first test query against the server:

Our GraphQL Playground at localhost:4000

Fetching real data

Now if we want to display actual data, not just mock data, we have to write some resolvers.

As we want to stay serverless, we use AWS DynamoDB as a data layer in this example. DynamoDB is a serverless NoSQL Database service from AWS, that can handle large amounts of data easily and where we only pay for what we use.

To communicate with the DynamoDB locally, we need a .env file where we add the following information:

Make sure the .env file is git ignored and never gets checked into version control, as it contains sensitive information.

Now let’s write our first resolver in resolvers/article.js.

We are using the DynamoDB documentClient here, which might be a bit cumbersome for larger apps. In bigger apps I would use [dynamodb toolbox](https://github.com/jeremydaly/dynamodb-toolbox), but for our example the documentClient is sufficient.

The transformer function might look something like this — this really depends on your use case! You might not even need one depending on your data model.

With our resolver in place, we can jump back into schema.js and actually use it:

We imported our resolver function and used it in the Query object of the resolvers object. the second argument, args, contains the request arguments, in this case id.

Nested resolvers, such as Product.price, get the parent object passed in as a first argument. This is pretty neat as it let’s us abstract oder schema further into re-usable sub types, such as Price.

Deployment to serverless

You can run sls deploy and the project gets packaged and deployed to AWS Lambda. Your output should look something like this:

serverless deployment

Conclusion

Serverless technologies are changing the way we develop our applications. The fact that we don’t need to think about infrastructure or optimize for later scaling gives us the possibility to laser focus on our apps. Another big plus is the low entry costs and not paying for any idle servers, we only pay for what is being executed. The downsides might be vendor lock-in into AWS, Google etc, because smaller businesses can just not compete with the services offered by large data centers of these providers.

You can find the code of this example on my Github.

Originally published at my personal Blog.

--

--