Create your first Serverless API

API's are what makes the internet work. They are the way your application interacts with your server so, as you might expect, having a solid API is crucial to the integrity of your application.

So what is an API?

API stands for Application Programming Interface and, at the risk of underselling its importance, it's a way of interacting with the server or a third party service through a series of predefined requests that usually return data.

I love the API analogies where they are compared to restaurant menus. You pick from a list of menu items and then the restaurant returns the order. Simple, right?

An API is an access point to an app that can access a database. Credits
When people speak of “an API”, they sometimes generalize and actually mean “a publicly available web-based API that returns data, likely in JSON or XML”. The API is not the database or even the server, it is the code that governs the access point(s) for the server.  - Perry Eising

How do you build an API?

Alright, we've established what an API is and how important it is for your application; let's see how we go about building one ourselves.

Since AWS Lambda is such a great fit for API's we'll be using that to build our first API. You can build one for free and, since you get the first million requests for free, it's going to make a good fit for your first API.

While you could do this whole thing without using a platform, we're going to use the most popular Serverless platform out there called, wait for it, the SERVERLESS platform!

I know what you're thinking, they are probably called that because they invented this whole Serverless thing but, no, they are called that just to mess with developers and confuse anyone just jumping on the Serverless bandwagon. </rant>

The first thing you need to do is install and configure your AWS and Serverless platform. To keep this short and sweet I'm going to link the easiest tutorial for setting up Serverless.

Let's get started with your first API

I'm assuming you've already installed serverless and you've hooked up your account. Up next we'll be installing all the necessary stuff in order to build our API - so let's jump straight in. All you need is a terminal, so open it up and type the following commands.

$ serverless create -t aws-nodejs -p rest-api 
$ cd rest-api

This will create a fresh boiler plate service. The second line is just navigating to the newly created folder

Now we need to install the modules we'll be using. I won't spend too much time explaining these since they are pretty self-explanatory.  

We install serverless-offline, to mimic the serverless platform, and you install mongoose for your database related needs. You'll need to make sure you are in the rest-api folder we created earlier.

$ npm init -y
$ npm i --save-dev serverless-offline
$ npm i --save mongoose dotenv

Next, you'll have to set up your database using Mongodb Atlas; a free service that you can use. We'll use this as a sandbox to play around with our API.

The setup isn't too complicated and should only take a few minutes. The first thing you need to do after you've set up your account is to create an organisation and give it a name. For the sake of keeping things clean and simple, we'll call it "rest-api".

After that, you need to create a project. Within the "organization" section hit "New Project", then create another project that we'll cleverly call "rest-api".

Then we need to create a cluster. From the project page, click the "Build a New Cluster" button that will take you to the cluster creation wizard. Here you'll select "M0", which is their free option. You'll want to disable backups, which you can do from the same page.

Don't forget to set your username and password and save them to a safe location. After that, we're on to writing some code.

Configure your Serverless framework

While we could do this without using the Serverless framework, one of the many perks of using the framework is the ease with which you setup a whole new project.

After initialising the platform (you already did this part earlier) you'll have a bunch of files in your folder. Amongst them, you'll see serverless.yml and handler.js.

Let's start with by opening up serverless.yml and adding the following code:

service: rest-api

provider:
  name: aws
  runtime: nodejs6.10 # set node.js runtime
  memorySize: 128 # set the maximum memory of the Lambdas in Megabytes
  timeout: 10 # the timeout is 10 seconds (default is 6 seconds)
  stage: dev # setting the env stage to dev, this will be visible in the routes
  region: us-east-1

functions: # add 4 functions for CRUD
  create:
    handler: handler.create # point to exported create function in handler.js
    events:
      - http:
          path: notes # path will be domain.name.com/dev/notes
          method: post
          cors: true
  getOne:
    handler: handler.getOne
    events:
      - http:
          path: notes/{id} # path will be domain.name.com/dev/notes/1
          method: get
          cors: true
  getAll:
    handler: handler.getAll # path will be domain.name.com/dev/notes
    events:
     - http:
         path: notes
         method: get
         cors: true
  update:
    handler: handler.update # path will be domain.name.com/dev/notes/1
    events:
     - http:
         path: notes/{id}
         method: put
         cors: true
  delete:
    handler: handler.delete
    events:
     - http:
         path: notes/{id} # path will be domain.name.com/dev/notes/1
         method: delete
         cors: true

plugins:
- serverless-offline # adding the plugin to be able to run the offline emulation

Without over complicating the tutorial, I'll quickly go over what we just did.

We named our service, you guessed it, "rest api" and configured our Lambdas to use a maximum of 128MB of Ram. You can go from 50 up to 3000mb but the more Ram you add, the more it will cost.

Next, we created a simple CRUD functionality using 5 functions: create, getOne, getAll, update and delete which will point to our handler.js file that we will get to in a second.

Last, but not least, is the serverless-offline plugin that we'll be using to test our service before going Live.

Adding logic to the functions

Our five functions don't need much to get going. Once we've defined what they do, all that's left to do is add the database connection and we're done. Here's the code you'll need for your handler.js file.

'use strict';

module.exports.create = (event, context, callback) => {
  context.callbackWaitsForEmptyEventLoop = false;

  connectToDatabase()
    .then(() => {
      Note.create(JSON.parse(event.body))
        .then(note => callback(null, {
          statusCode: 200,
          body: JSON.stringify(note)
        }))
        .catch(err => callback(null, {
          statusCode: err.statusCode || 500,
          headers: { 'Content-Type': 'text/plain' },
          body: 'Could not create the note.'
        }));
    });
};

module.exports.getOne = (event, context, callback) => {
  context.callbackWaitsForEmptyEventLoop = false;

  connectToDatabase()
    .then(() => {
      Note.findById(event.pathParameters.id)
        .then(note => callback(null, {
          statusCode: 200,
          body: JSON.stringify(note)
        }))
        .catch(err => callback(null, {
          statusCode: err.statusCode || 500,
          headers: { 'Content-Type': 'text/plain' },
          body: 'Could not fetch the note.'
        }));
    });
};

module.exports.getAll = (event, context, callback) => {
  context.callbackWaitsForEmptyEventLoop = false;

  connectToDatabase()
    .then(() => {
      Note.find()
        .then(notes => callback(null, {
          statusCode: 200,
          body: JSON.stringify(notes)
        }))
        .catch(err => callback(null, {
          statusCode: err.statusCode || 500,
          headers: { 'Content-Type': 'text/plain' },
          body: 'Could not fetch the notes.'
        }))
    });
};

module.exports.update = (event, context, callback) => {
  context.callbackWaitsForEmptyEventLoop = false;

  connectToDatabase()
    .then(() => {
      Note.findByIdAndUpdate(event.pathParameters.id, JSON.parse(event.body), { new: true })
        .then(note => callback(null, {
          statusCode: 200,
          body: JSON.stringify(note)
        }))
        .catch(err => callback(null, {
          statusCode: err.statusCode || 500,
          headers: { 'Content-Type': 'text/plain' },
          body: 'Could not fetch the notes.'
        }));
    });
};

module.exports.delete = (event, context, callback) => {
  context.callbackWaitsForEmptyEventLoop = false;

  connectToDatabase()
    .then(() => {
      Note.findByIdAndRemove(event.pathParameters.id)
        .then(note => callback(null, {
          statusCode: 200,
          body: JSON.stringify({ message: 'Removed note with id: ' + note._id, note: note })
        }))
        .catch(err => callback(null, {
          statusCode: err.statusCode || 500,
          headers: { 'Content-Type': 'text/plain' },
          body: 'Could not fetch the notes.'
        }));
    });
};

To anyone that has ever written a nodejs function before, this should be easy to understand. There is no special logic here; we define the five functions mentioned earlier and add the logic for each one.

Database connection setup

There are two steps involved here, one is to create a dynamic connection and the second one is to have a way to reuse the same connection if available. Sounds tricky but I'll walk you through it. What you want to do is add another file next to your handler.js file - let's call it database.js, then paste the following code:

const mongoose = require('mongoose');
mongoose.Promise = global.Promise;
let isConnected;

module.exports = connectToDatabase = () => {
  if (isConnected) {
    console.log('=> using existing database connection');
    return Promise.resolve();
  }

  console.log('=> using new database connection');
  return mongoose.connect(process.env.DB)
    .then(db => { 
      isConnected = db.connections[0].readyState;
    });
};

Note: This syntax is valid for Mongoose 5.0.0-rc0 and above. It will not work with any version of Mongoose which is lower than 5.

We’re basically caching the database connection, making sure it’ll not get created if it already exists. In that case, we just resolve the promise right away.

With the databse.js file created, let’s require it in the handler.js. Just add this snippet to the top of the handler.

// top of handler.js
const connectToDatabase = require('./db');

Creating the Note model

We've referenced the note model in the handler.js file earlier, so we'll have to create that too. We'll create a folder in the root directory called models and in that create a Note.js file. Paste the following code in there.

const mongoose = require('mongoose');
const NoteSchema = new mongoose.Schema({  
  title: String,
  description: String
});
module.exports = mongoose.model('Note', NoteSchema);

Now we have to export the model in our handler.js file, which we do by adding the following code at the top of the file.

// top of handler.js
const connectToDatabase = require('./db');
const Note = require('./models/Note');

Creating the dotenv for environment variables

Leaving config files and keys in a totally separate file is incredibly easy with dotenv, and a real life saver. You can just add the file to .gitignore and be sure you won’t risk compromising any keys.

You do this by creating a new file called variable.env and place it in the root directory.

DB=mongodb://<user>:<password>@mongodb.net:27017/db

To get the info you need you'll have to go back to Atlas and look at the cluster page. Click on the connect button and then grab the URL by hitting the "Connect your application" button.

After you’ve pressed “Connect Your Application” you’ll be prompted to “Copy a connection string”. Press “I am using driver 3.4 or earlier” and you can FINALLY copy the URL. Whoa, that was a tiresome ride.

Once you’ve copied it, head back to the variables.env file and add the actual connection URL.

DB=mongodb://dbadmin:[email protected]:27017,cluster0-shard-00-01-e9ai4.mongodb.net:27017,cluster0-shard-00-02-e9ai4.mongodb.net:27017/test?ssl=true&replicaSet=Cluster0-shard-0&authSource=admin

Just replace the password and url to whatever you've used in Atlas and you are done.

Note: Don’t forget to add the variables.env to the .gitignore!

Lastly, before jumping into testing everything out, we need to require the dotenv module and point to the file where we’re keeping the environment variables. Add this snippet to the top of your handler.js file.

require('dotenv').config({ path: './variables.env' });

Running the API

At this point, we should be ready to test our API and to do so we need to run the serverless-offline module and, because we are using the Mongoose model definition, there's a flag we need to trigger when running.

$ serverless offline start --skipCacheInvalidation

Once you’ve run the command in the terminal, you should see something like this.

To actually add a new note you can use Postman to POST a request to the following endpoint:

http://localhost:3000/notes

with a body that contains the title and description:

{
    "title": "some title"
    "description: "some description"
}

The response should look like this.

{
    "_id": "48hfs9da3f9jfdhs8g"
    "title": "some title"
    "description: "some description"
    "__v":0
}

Deployment

Alright, we've seen the API work locally with serverless-offline, and played with the routes a little bit. Now let's deploy this to AWS Lambda to see how it works live. All you need to do is run this command:

serverless deploy

This will provision all the AWS resources and upload the code to S3 from where it will be picked up by Lambda. It's going to take a few seconds but what you should be seeing is something like this:

Once it's done you'll be able to access the requests by using the endpoints written in the log. If you are going to mess with the API you can just redeploy using serverless deploy

It's imperative you understand how AWS Lambda works and get acquainted with the limits in order to create functions that are performing well. I'd suggest doing a little bit of homework on AWS Lambda before you jump right in.

Final thoughts

I have my doubts about how many people will take the time to go through all of these steps but I want to congratulate any that do. Creating an API is serious business and will take some time to get it right but it's a crucial component of any successful application.

While this tutorial revolved around the Serverless Platform, I do want to point out that there are other options out there. As an alternative to Serverless, I'd look into Zappa, they are newer to the Serverless scene but the guys behind the project are doing some really cool stuff with Python.

Also, AWS Lambda is not your only option, Microsoft Azure and Google Cloud Functions are solid options as well. One thing I'd do before jumping the gun is to look closely at what my project requires and make a decision based on the other services available from the cloud provider.


I want to give a shoutout to Adnan Rahic who helped me create this tutorial. He writes a lot of cool stuff related to Serverless and I can't recommend him enough to anyone serious about Serverless development. He spent the better part of the past 2 months writing this amazing book on NodeJS monitoring, I suggest you check it out.

Credits for the cool featured image go to Markus Spiske on Unsplash.

Previous Post Next Post