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 :
- Typescript
- AWS Lambda
- GraphQl(Apollo)
- Serverless Framework
- Redis
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.
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