Building my first API using Serverless, Typescript, and GraphQl

Serverless is a new trend for the deployment of cloud applications. Recently it has gained much popularity mainly due to the shift of enterprise application architectures to containers and microservices.
The “serverless” term clearly does not mean that our services no longer need servers to run on, but means that we no longer need provisioning of infrastructure and ongoing management of our servers. This in addition to the on-demand billing system, cuts down dramatically the cost related to compute resources.

The popularity of “serverless” as reported by Google Trends

The serverless framework is a recent development in the serverless ecosystem. It is an open-source CLI for building and deploying serverless applications, with over 6 million deployments handled to date.
Serverless is provider agnostic, so you only have to develop one version of your application to run anywhere you want it to.

Getting started with Serverless

In this tutorial, we are using :

First, let’s install the Serverless framework on our machine.

$ npm install -g serverless
# Login to the serverless platform (optional)
$ serverless login

Next, let’s set up our AWS credentials.

Open AWS console. go to Security, Identity, & Compliance, IAM, Users. Create a new user with programmatic access(the one that enables an access key ID and secret access key). Click on next to set up permissions, attach existing policies and give it “administrator access”. Skip tags, and then click on “Create User”. We get a pair of keys that we have to export as environment variables on our machine so they would be accessible to Serverless and the AWS SDK in your shell.

Now save these credentials by running this in your terminal:

$ serverless config credentials --provider aws --key YOUR_ACCESS_KEY --secret YOUR_SECRET_KEY

Creating our Boilerplate

Now that we have our environment ready let’s start by creating the boilerplate of our application.

Serverless provides a variety of examples to generate automatically, for our case we are going to use the aws/typescript one.

One thing to add, we want to be able to test our application locally before deploying it to Amazon Lambda, so we’re going to use a package that emulates serverless behavior on your machine.

We have to add serverless-offline to the list of plugins in our YAML file. It should look something like this:

*(serverless.yml)*
plugins:
  - serverless-webpack
  - serverless-offline

Now our service is ready to run and receive its first request.

$ sls offline start --port 8080

Running the previous will get you something like this:

Starting serverless Locally

Enter http://localhost:3000/hello in your browser:

Woohoo, you just made your first serverless function.

If you look at our serverless.yml file, you’ll notice that we have a function called Hello and it’s triggered by an HTTP event.

Integrating Apollo(GraphQl)

First Install Apollos’ package for Lambda:

npm install apollo-server-lambda@rc graphql --save

Change your Handler.ts to this:

import { ApolloServer, gql } from 'apollo-server-lambda';
import { CommentService } from './comment.service';

const typeDefs = gql`
  type Comment{
    msgId: Int
    userId : String
    content : String
    createdAt : String
    deleted : Boolean
  }
  type Query {
    get(itemId: String): [Comment]
  }
  type Mutation {
    add(itemId: String, userId:String, content:String): [Comment]
    edit(itemId: String, msgId:Int,userId:String, content:String): [Comment]
    delete(itemId: String, msgId:Int, userId:String) : [Comment]
  }
`;

const resolvers = {
  Query: {
    get: (root, args) => {
      const service = new CommentService();
      return service.getComments(args.itemId);
    },
  },
  Mutation: {
    add: (roots, args) => {
      const service = new CommentService();
      return service.addComments(args.itemId, args.userId, args.content);
    },
    edit: (roots, args) => {
      const service = new CommentService();
      return service.editComments(args.itemId, args.msgId, args.userId, args.content);
    },
    delete: (roots, args) => {
      const service = new CommentService();
      return service.deleteComments(args.itemId, args.msgId, args.userId);
    }
  }
};

const server = new ApolloServer({
  typeDefs,
  resolvers,
});

exports.graphqlHandler = server.createHandler();

and update serverless.yml:

service:
  name: aws-typescript-graphql-tuto

# Add the serverless-webpack plugin
plugins:
  - serverless-webpack
  - serverless-offline

provider:
  name: aws
  runtime: nodejs8.10
  stage: ${opt:stage, 'dev'}
  environment:
    redisHost: '${file(./config/${self:provider.stage}.json):redis.host}'
    redisPort: '${file(./config/${self:provider.stage}.json):redis.port}'


functions:
  graphql:
    handler: handler.graphqlHandler
    events:
      - http:
          method: post
          path: graphql

One more thing, in your _webpack.config.js _, you have to add one little change. Otherwise you’ll be flooded with errors 😆:

add ‘.mjs’ to the beginning of the extensions list

resolve: {

extensions: ['.mjs','.js', '.jsx', '.json', '.ts', '.tsx'],

}

Our second part is now finished, we only have to test our work.

GraphQl Playground:

Running a Redis image on EC2

In the following, we are going to create an EC2 instance and start a Redis instance with Docker.

There are a lot of guides out there showing how to create an EC2 instance, so we are not going to delve into that(just make sure you choose a Linux instance, I’m choosing Ubuntu Server 18.04 LTS).

Once you got your instance ready, you want to connect to it using SSH. In the following, I am going to use Ubuntu, for windows users you can use PuTTY.

From the directory where you saved your private key file, open a terminal and run the following command.

ssh -i "keyName.pem" [your_instance_host](http://xxx.compute.amazonaws.com)

Connect to the EC2 instance

Then install Docker:

$ curl -fsSL https://download.docker.com/linux/ubuntu/gpg | sudo apt-key add -
$ sudo add-apt-repository "deb [arch=amd64] [https://download.docker.com/linux/ubuntu](https://download.docker.com/linux/ubuntu) $(lsb_release -cs) stable"
$ sudo apt-get update
$ apt-cache policy docker-ce
$ sudo apt-get install -y docker-ce

Docker is now installed, the daemon started, and the process enabled to start on boot. Check that it’s running:

$ sudo systemctl status docker

Docker running on EC2

Let’s run a Redis image:

$ docker run --volume /docker/redis-data:/data -p 6379:6379 -d redis   redis-server --appendonly yes

Redis Image on Docker

Let’s check it:

$ sudo docker container ls

Redis Running on EC2

Building our API

Handler.ts:

To begin, let’s import Apollo Server for lambda:

import { ApolloServer, gql } from 'apollo-server-lambda';

Next, we define our queries and **mutations. **We are going to create a commenting service; hence we have a “Comment” type.

const typeDefs = gql`
  type Comment{
    msgId: Int
    userId : String
    content : String
    createdAt : String
    deleted : Boolean
  }
  type Query {
    get(itemId: String): [Comment]
  }
  type Mutation {
    add(itemId: String, userId:String, content:String): [Comment]
    edit(itemId: String, msgId:Int,userId:String, content:String): [Comment]
    delete(itemId: String, msgId:Int, userId:String) : [Comment]
  }
`;

The last thing is our resolvers:

const resolvers = {
  Query: {
    get: (root, args) => {
      //get from comment service
    },
  },
  Mutation: {
    add: (roots, args) => {
      //add from comment service
    },
    edit: (roots, args) => {
      //edit from comment service
    },
    delete: (roots, args) => {
      //delete from comment service
    }
  }
};e

Before creating our commenting service, let’s create a service that connects to the Redis database(comment.storage.ts).

Let’s install Redis

npm i redis --save

Add a file called* types.ts *to the root of your project:

export interface Comment {
    msgId: number,
    userId: string,
    content: string,
    createdAt: number,
    deleted: Boolean
}

then** ***comment.storage.ts:*

import * as redis from 'redis'
import { promisify } from 'util';
import { Comment } from './types'

const redisOptions = {
  host: process.env.redisHost,
  port: process.env.redisPort,
}
let client;


export interface ICommentStorage {

  get(key): Promise<[Comment]>;
  add(key: string, userId: string, content: string): Promise<[Comment]>;
  edit(key: string, msgId: number, userId: string, content: string): Promise<[Comment]>;
  delete(key: string, msgId: number, userId: string): Promise<[Comment]>;

}

export class CommentStorage {

  // CRUD functions
  // Get Comments : fetch all comments related to it the module(rfq,quote)
  get(key): Promise<[Comment]> {


    client = redis.createClient(redisOptions.port, redisOptions.host);
    const getAsync = promisify(client.lrange).bind(client);

    client.on('ready', () => {
      console.log('redis is ready.');
    });
    client.on('end', () => {
      console.log('redis closed.');
    });
    return getAsync(key, 0, -1).then((res) => {
      return res
        .map(row => JSON.parse(row))
        .filter(row => (row.deleted == false));
    });
  }
  getNotFiletered(key): Promise<[Comment]> {
    client = redis.createClient(redisOptions.port, redisOptions.host);
    const getAsync = promisify(client.lrange).bind(client);

    return getAsync(key, 0, -1).then((res) => {
      return res
        .map(row => JSON.parse(row));
    });
  }
  // Add Comments : appends a comment to the list for a specific item
  async add(key, userId, content): Promise<[Comment]> {

    client = redis.createClient(redisOptions.port, redisOptions.host);
    const lindexAsync = promisify(client.lindex).bind(client);
    let lastElement = await lindexAsync(key, -1);
    let new_msgId = 0;
    if (lastElement != null) {
      lastElement = JSON.parse(lastElement);
      new_msgId = lastElement.msgId + 1;
    }

    let comment: Comment = {
      msgId: new_msgId,
      userId: userId,
      content: content,
      createdAt: Date.now(),
      deleted: false
    }
    client = redis.createClient(redisOptions.port, redisOptions.host);
    const addAsync = promisify(client.rpush).bind(client);
    let value = JSON.stringify(comment);

    return addAsync(key, value).then((status) => {
      return this.get(key);
    });
  }
  // Edit Comments  : loop through comments and edit the one with the specific msgId
  async edit(key, msgId, userId, content): Promise<[Comment]> {

    client = redis.createClient(redisOptions.port, redisOptions.host);
    const setAsync = promisify(client.lset).bind(client);
    let comments = await this.getNotFiletered(key);

    for (let i = 0; i < comments.length; i++) {
      if (comments[i].msgId == msgId && comments[i].userId == userId) {
        if (content) comments[i].content = content;
        setAsync(key, i, JSON.stringify(comments[i]));
        
      }
    }

    return this.get(key);
  }
  //Delete Comments : loop through the comments and delete the comment with specific msgId
  async delete(key, msgId, userId): Promise<[Comment]> {

    client = redis.createClient(redisOptions.port, redisOptions.host);
    const setAsync = promisify(client.lset).bind(client);
    let comments = await this.getNotFiletered(key);

    for (let i = 0; i < comments.length; i++) {
      if (comments[i].msgId === msgId && userId == comments[i].userId
        && comments[i].deleted == false) {

        comments[i].deleted = true;
        setAsync(key, i, JSON.stringify(comments[i]));

      }
    }

    return this.get(key);
  }
}

Now let’s implement our comment.service.ts:

import { Comment } from './types';
import { ICommentStorage, CommentStorage } from './comment.storage';

export interface ICommentService {

  getComments(itemId: string): Promise<[Comment]>;
  addComments(itemId: string, userId: string, content: string): Promise<[Comment]>;
  editComments(itemId: string, msgId: number, userId: string, content: string): Promise<[Comment]>;
  deleteComments(itemId: string, msgId: number, userId: string): Promise<[Comment]>;

}


export class CommentService implements ICommentService {

  commentStore: ICommentStorage;

  constructor(store?: ICommentStorage) {
    if (store) {
      this.commentStore = store
    } else {
      this.commentStore = new CommentStorage();
    }
  }

  getComments(itemId): Promise<[Comment]> {


    return this.commentStore.get(itemId);
  }

  addComments(itemId, userId, content): Promise<[Comment]> {

    return this.commentStore.add(itemId, userId, content);
  }

  editComments(itemId, msgId, userId, content): Promise<[Comment]> {

    return this.commentStore.edit(itemId, msgId, userId, content);
  }

  deleteComments(itemId, msgId, userId): Promise<[Comment]> {

    return this.commentStore.delete(itemId, msgId, userId);
  }
}

It doesn’t have much, but this service is going to be useful if we want to add some logic and filtering.

Return to our *Handler.ts. *We are going to implement the missing functions:

import { ApolloServer, gql } from 'apollo-server-lambda';
import { CommentService } from './comment.service';

const typeDefs = gql`
  type Comment{
    msgId: Int
    userId : String
    content : String
    createdAt : String
    deleted : Boolean
  }
  type Query {
    get(itemId: String): [Comment]
  }
  type Mutation {
    add(itemId: String, userId:String, content:String): [Comment]
    edit(itemId: String, msgId:Int,userId:String, content:String): [Comment]
    delete(itemId: String, msgId:Int, userId:String) : [Comment]
  }
`;

const resolvers = {
  Query: {
    get: (root, args) => {
      const service = new CommentService();
      return service.getComments(args.itemId);
    },
  },
  Mutation: {
    add: (roots, args) => {
      const service = new CommentService();
      return service.addComments(args.itemId, args.userId, args.content);
    },
    edit: (roots, args) => {
      const service = new CommentService();
      return service.editComments(args.itemId, args.msgId, args.userId, args.content);
    },
    delete: (roots, args) => {
      const service = new CommentService();
      return service.deleteComments(args.itemId, args.msgId, args.userId);
    }
  }
};

const server = new ApolloServer({
  typeDefs,
  resolvers,
});

exports.graphqlHandler = server.createHandler();

Now our logic is complete, all we have to do is configure Redis port and host.

Let’s create a config folder, inside create a* prod.json*, u can create later a file for every stage(maybe one for a local development environment with a local Redis instance), we’re going to see how to configure that in our serverless.yml.

*(prod.json)*

{
    "redis":{
        "host" : "Your EC2 instance Public IP",
        "port" : "6379"
    }
}

ps: ec2–a–b–c–d.ec2_region.compute.amazonaws.com then your IP is a.b.c.d

otherwise just check it out on the aws console.

Check the connection to your Redis database.

$ sudo apt-get install redis-tools

then test the connection:

Change your serverless.yml, so it can grab the specific configuration file when stage changes.

Fire up your local serverless function

sls offline start

and test your functions:

Feel free to test the other functions as you wish.

Finally, our app is done 😎 ! Let’s deploy it.

ps: if you face an error like this one

Module not found: Error: Can't resolve 'hiredis'...

Create a folder named “aliases” at the root of your project, within this folder create a file hiredis.js with the following content:

export default null

and then add this to your webpack.config.js:

extensions: .............................,

alias: {

'hiredis': path.join(__dirname, 'aliases/hiredis.js')

}

},

Deploying to AWS Lambda

Were you afraid of the deployment? Let me show you how easy it is to deploy your app with Serverless.

Literally, all you have to do is type in your terminal:

sls deploy -v --stage prod

Grab the endpoint and test your service :D

GitHub - AyoubEd/serverless-graphql-api: Serverless backend with GraphQl to a comment section
Serverless backend with GraphQl to a comment section - GitHub - AyoubEd/serverless-graphql-api: Serverless backend with GraphQl to a comment section