Building a REST API in Node.js with AWS Lambda · API Gateway and Serverless Framework

Building a REST API in Node.js with AWS Lambda, API Gateway and Serverless Framework

Prerequisite

  • AWS account
  • Node.js
  • AWS CLI and configure it

Getting Started with the Serverless Framework

To install Serverless on your machine, run the below mentioned npm command.

use sudo if required, for my mac book to install serverless globally, I used sudo

$ npm install serverless -g

This will install Serverless command-line on your machine. You can use sls alias instead of typing serverless as well.

Now, we will build a POST REST API application in a step by step manner.

Step 1: Create a Node.js Serverless Project

Go to a convenient location on your filesystem and create a directory. For my example I am creating a directory called registration_desk

$mkdir registration_desk; cd registration_desk;

Once inside the registration_desk directory, we’ll start our first micro-service for working with students. This will be responsible for saving student details, listing students, and fetching a single student details.

$ serverless create --template aws-nodejs --path student-service --name student

This will create a directory student-service with the following structure.

.
├── .gitignore
├── handler.js
└── serverless.yml

A short description of each file

  • ** .gitignore ** : I think there is no need to explain the requirement of this file.

  • ** handler.js ** : This declares your Lambda function. The created Lambda function returns a body with Go Serverless v1.0! Your function executed successfully! message.

  • ** serverless.yml ** : This file declares configuration that Serverless Framework uses to create your service. ** serverless.yml ** file has three sections — provider, functions, and resources.

    • ** provider ** : This section declares configuration specific to a cloud provider. You can use it to specify name of the cloud provider, region, runtime etc.

    • ** functions ** : This section is used to specify all the functions that your service is composed off. A service can be composed of one or more functions.

    • ** resources **: This section declares all the resources that your functions use. Resources are declared using AWS CloudFormation.

Step 2: Create a REST Resource for Submitting students.

Next, we’ll update the default serverless.yml as shown below.

service: student-service

provider:
  name: aws
  runtime: nodejs12.x
  stage: dev
  region: us-east-1

functions:
  studentSubmission:
    handler: api/student.submit
    memorySize: 128
    description: Submit student information and start process.
    events:
      - http: 
          path: students
          method: post

Let’s go over the YAML configuration:

  • We defined name of the service -- student-service. Service name has to be unique for your account.

  • Next, we defined configuration of the cloud provider. As we are using AWS so we defined AWS corresponding configuration.

  • Finally, we defined studentSubmission function. In the configuration shown above, we declared that when the HTTP POST request is made to /students then api/student.submit handler should be invoked. We also specified memory we want to allocate to the function.

Now, create a new directory api inside the student-service directory. Move the handler.js to the api directory. Rename handler.js to student.js and rename hello to submit.

Step 3: Let’s configure AWS credentials to deploy the function we created

Execute below command to configure AWS credentials with a profile name

$ serverless config credentials --provider aws --key AXXXXXXXXXXXXX --secret Nxxxxxxxxxxxxxxxxxxxxxxxxx+ --profile teacher

Step 4: Deploying the endpoint with Lambda function in AWS

To validate the serverless.yaml file without deployment, execute below command

$ serverless deploy --noDeploy --stage dev --aws-profile=teacher

To deploy to cloud use below command

sls deploy --stage dev --aws-profile=teacher

On successful deployment, you will get a message like this

Serverless: Packaging service...
Serverless: Excluding development dependencies...
Serverless: Creating Stack...
Serverless: Checking Stack create progress...
........
Serverless: Stack create finished...
Serverless: Uploading CloudFormation file to S3...
Serverless: Uploading artifacts...
Serverless: Uploading service student-service.zip file to S3 (398 B)...
Serverless: Validating template...
Serverless: Updating Stack...
Serverless: Checking Stack update progress...
..............................
Serverless: Stack update finished...
Service Information
service: student-service
stage: dev
region: us-east-1
stack: student-service-dev
resources: 11
api keys:
  None
endpoints:
  POST - https://xxxx.execute-api.us-east-1.amazonaws.com/dev/students
functions:
  studentSubmission: student-service-dev-studentSubmission
layers:
  None
Serverless: Run the "serverless" command to setup monitoring, troubleshooting and testing.

Now, POST operation of your service is available. You can use tools like cURL to make a POST request.

$curl -X POST https://xxxx.execute-api.us-east-1.amazonaws.com/dev/students

You should see the output like this,

{
  "message": "Go Serverless v1.0! Your function executed successfully!",
  "input": {
    "resource": "/students",
    "path": "/students",
    "httpMethod": "POST",
    "headers": {
      "Accept": "*/*",
      "CloudFront-Forwarded-Proto": "https",
      "CloudFront-Is-Desktop-Viewer": "true",
      "CloudFront-Is-Mobile-Viewer": "false",
      "CloudFront-Is-SmartTV-Viewer": "false",
      "CloudFront-Is-Tablet-Viewer": "false",
      "CloudFront-Viewer-Country": "IN",
      "Host": "xxxx.execute-api.us-east-1.amazonaws.com",
      "User-Agent": "curl/7.64.1",
      "Via": "2.0 xxxxx.cloudfront.net (CloudFront)",
      "X-Amz-Cf-Id": "hhkjdhkjhkjhkjhkjhkjhkjhkjhk",
      "X-Amzn-Trace-Id": "Root=1-hkjhkjhkjhkjhkjh",
      "X-Forwarded-For": "49.x.217.40, 64.252.145.69",
      "X-Forwarded-Port": "443",
      "X-Forwarded-Proto": "https"
    },

#Step 5: Add Student info and store it in DB (Dynamo DB)

Now that we are able to make HTTP POST request to our API let’s update the code so that data can be saved to DynamoDB. We’ll start by adding iamRoleStatemements to serverless.yml. This defines which actions are permissible.

service: student-service

provider:
  name: aws
  runtime: nodejs12.x
  stage: dev
  region: us-east-1
  environment:
    STUDENT_TABLE: ${self:service}-${opt:stage, self:provider.stage}
    STUDENT_EMAIL_TABLE: "student-email-${opt:stage, self:provider.stage}"
  iamRoleStatements:
    - Effect: Allow
      Action:
        - dynamodb:Query
        - dynamodb:Scan
        - dynamodb:GetItem
        - dynamodb:PutItem
      Resource: "*"

functions:
  studentSubmission:
    handler: api/student.submit
    memorySize: 128
    description: Submit student information and start process.
    events:
      - http: 
          path: students
          method: post

Next, we’ll create a resource that will create DynamoDB table as shown below.

service: student-service

provider:
  name: aws
  runtime: nodejs12.x
  stage: dev
  region: us-east-1
  environment:
    STUDENT_TABLE: ${self:service}-${opt:stage, self:provider.stage}
    STUDENT_EMAIL_TABLE: "student-email-${opt:stage, self:provider.stage}"
  iamRoleStatements:
    - Effect: Allow
      Action:
        - dynamodb:Query
        - dynamodb:Scan
        - dynamodb:GetItem
        - dynamodb:PutItem
      Resource: "*"

functions:
  studentSubmission:
    handler: api/student.submit
    memorySize: 128
    description: Submit student information and start process.
    events:
      - http: 
          path: students
          method: post


resources:
  Resources:
    StudentsDynamoDbTable:
      Type: 'AWS::DynamoDB::Table'
      DeletionPolicy: Retain
      Properties:
        AttributeDefinitions:
          -
            AttributeName: "id"
            AttributeType: "S"   
        KeySchema:
          -
            AttributeName: "id"
            KeyType: "HASH"
        ProvisionedThroughput:
          ReadCapacityUnits: 1
          WriteCapacityUnits: 1
        StreamSpecification:
          StreamViewType: "NEW_AND_OLD_IMAGES"
        TableName: ${self:provider.environment.STUDENT_TABLE}

Now, install a couple of node dependencies. These will be required by our code.

$ npm install --save bluebird
$ npm install --save uuid

Update the api/student.js as shown below.

'use strict';

const uuid = require('uuid');
const AWS = require('aws-sdk'); 

AWS.config.setPromisesDependency(require('bluebird'));

const dynamoDb = new AWS.DynamoDB.DocumentClient();

module.exports.submit = (event, context, callback) => {
  const requestBody = JSON.parse(event.body);
  const fullname = requestBody.fullname;
  const email = requestBody.email;
  const experience = requestBody.experience;

  if (typeof fullname !== 'string' || typeof email !== 'string' || typeof experience !== 'number') {
    console.error('Validation Failed');
    callback(new Error('Couldn\'t submit student because of validation errors.'));
    return;
  }

  submitStudentP(studentInfo(fullname, email, experience))
    .then(res => {
      callback(null, {
        statusCode: 200,
        body: JSON.stringify({
          message: `Sucessfully submitted student with email ${email}`,
          studentId: res.id
        })
      });
    })
    .catch(err => {
      console.log(err);
      callback(null, {
        statusCode: 500,
        body: JSON.stringify({
          message: `Unable to submit student with email ${email}`
        })
      })
    });
};


  const submitStudentP = student => {
  console.log('Submitting student');
  const studentInfo = {
    TableName: process.env.STUDENT_TABLE,
    Item: student,
  };
  return dynamoDb.put(studentInfo).promise()
    .then(res => student);
};

const studentInfo = (fullname, email, experience) => {
  const timestamp = new Date().getTime();
  return {
    id: uuid.v1(),
    fullname: fullname,
    email: email,
    experience: experience,
    submittedAt: timestamp,
    updatedAt: timestamp,
  };
};

Now, we can deploy the function as shown below. Before running actual deploy do a dryrun to validate the serverless.yaml with below command

serverless deploy --noDeploy --stage dev --aws-profile=teacher

Now, let’s run the actual deploy command with verbose option

sls deploy -v --stage dev --aws-profile=teacher

This will create the DynamoDB table.

On successful run of the above command, you should see output as below

Serverless: Packaging service...
Serverless: Excluding development dependencies...
Serverless: Uploading CloudFormation file to S3...
Serverless: Uploading artifacts...
Serverless: Uploading service student-service.zip file to S3 (207.26 KB)...
Serverless: Validating template...
Serverless: Updating Stack...
Serverless: Checking Stack update progress...
CloudFormation - UPDATE_IN_PROGRESS - AWS::CloudFormation::Stack - student-service-dev
CloudFormation - CREATE_IN_PROGRESS - AWS::DynamoDB::Table - StudentsDynamoDbTable
CloudFormation - UPDATE_IN_PROGRESS - AWS::IAM::Role - IamRoleLambdaExecution
CloudFormation - CREATE_IN_PROGRESS - AWS::DynamoDB::Table - StudentsDynamoDbTable
CloudFormation - UPDATE_COMPLETE - AWS::IAM::Role - IamRoleLambdaExecution
CloudFormation - UPDATE_IN_PROGRESS - AWS::Lambda::Function - StudentSubmissionLambdaFunction
CloudFormation - UPDATE_COMPLETE - AWS::Lambda::Function - StudentSubmissionLambdaFunction
CloudFormation - CREATE_IN_PROGRESS - AWS::ApiGateway::Deployment - ApiGatewayDeploymentXXXXXXXXXXXXXX
CloudFormation - CREATE_IN_PROGRESS - AWS::Lambda::Version - StudentSubmissionLambdaVersionZZZZZZZZZZZZZZZZZ
CloudFormation - CREATE_IN_PROGRESS - AWS::ApiGateway::Deployment - ApiGatewayDeploymentXXXXXXXXXXXXXX
CloudFormation - CREATE_IN_PROGRESS - AWS::Lambda::Version - StudentSubmissionLambdaVersionZZZZZZZZZZZZZZZZZ
CloudFormation - CREATE_COMPLETE - AWS::ApiGateway::Deployment - ApiGatewayDeploymentXXXXXXXXXXXXXX
CloudFormation - CREATE_COMPLETE - AWS::Lambda::Version - StudentSubmissionLambdaVersionZZZZZZZZZZZZZZZZZ
CloudFormation - CREATE_COMPLETE - AWS::DynamoDB::Table - StudentsDynamoDbTable
CloudFormation - UPDATE_COMPLETE_CLEANUP_IN_PROGRESS - AWS::CloudFormation::Stack - student-service-dev
CloudFormation - DELETE_IN_PROGRESS - AWS::ApiGateway::Deployment - ApiGatewayDeploymentYYYYYYYYYYYYYYYY
CloudFormation - DELETE_SKIPPED - AWS::Lambda::Version - StudentSubmissionLambdaVersionqXE4Q5S94g2CUqJpz1D8RSSRG7RQgpo5JxEB4BDFxMg
CloudFormation - DELETE_COMPLETE - AWS::ApiGateway::Deployment - ApiGatewayDeploymentYYYYYYYYYYYYYYYY
CloudFormation - UPDATE_COMPLETE - AWS::CloudFormation::Stack - student-service-dev
Serverless: Stack update finished...
Service Information
service: student-service
stage: dev
region: us-east-1
stack: student-service-dev
resources: 12
api keys:
  None
endpoints:
  POST - https://xxxx.execute-api.us-east-1.amazonaws.com/dev/students
functions:
  studentSubmission: student-service-dev-studentSubmission
layers:
  None

Stack Outputs
StudentSubmissionLambdaFunctionQualifiedArn: arn:aws:lambda:us-east-1:XXXXXXX:function:student-service-dev-studentSubmission:2
ServiceEndpoint: https://xxxx.execute-api.us-east-1.amazonaws.com/dev
ServerlessDeploymentBucketName: student-service-dev-serverlessdeploymentbucket-xxxxxxx

To test the API, we can use cURL again.

$ curl -H "Content-Type: application/json" -X POST -d '{"fullname":"Leeladharan M P","email": "leelu123@gmail.com", "experience":12}' https://xxxx.execute-api.us-east-1.amazonaws.com/dev/students

On successful execution of above command, below should be the output

{
    "message":"Sucessfully submitted student with email leelu123@gmail.com",
    "studentId":"827c6300-3a86-11ea-9d97-4393a018ce9f"
}

#Step 4: Get All Students

Define a new function in the serverless.yml as shown below.

listStudents:
    handler: api/student.list
    memorySize: 128
    description: List all students
    events:
      - http: 
          path: students
          method: get

Create new function in the api/student.js as shown below.

module.exports.list = (event, context, callback) => {
  var params = {
      TableName: process.env.STUDENT_TABLE,
      ProjectionExpression: "id, fullname, email"
  };

  console.log("Scanning Student table.");
  const onScan = (err, data) => {

      if (err) {
          console.log('Scan failed to load data. Error JSON:', JSON.stringify(err, null, 2));
          callback(err);
      } else {
          console.log("Scan succeeded.");
          return callback(null, {
              statusCode: 200,
              body: JSON.stringify({
                  students: data.Items
              })
          });
      }

  };

  dynamoDb.scan(params, onScan);

};

Deploy the function again.

sls deploy -v --stage dev --aws-profile=teacher

Once deployed we will be able to test the API using cURL.

curl -X GET https://xxxx.execute-api.us-east-1.amazonaws.com/dev/students

the output will be,

{
    "students":
            [
                 {
                      "email":"leelu123@gmail.com",
                      "id":"827c6300-3a86-11ea-9d97-4393a018ce9f",
                      "fullname":"Leeladharan M P"
                 }
           ]
}

Step 5: Get Student Details by ID

Define a new function in serverless.yml as shown below.

studentDetails:
    handler: api/student.get
    events:
      - http:
          path: students/{id}
          method: get

Define a new function in api/student.js

module.exports.get = (event, context, callback) => {
  const params = {
    TableName: process.env.STUDENT_TABLE,
    Key: {
      id: event.pathParameters.id,
    },
  };

  dynamoDb.get(params).promise()
    .then(result => {
      const response = {
        statusCode: 200,
        body: JSON.stringify(result.Item),
      };
      callback(null, response);
    })
    .catch(error => {
      console.error(error);
      callback(new Error('Couldn\'t fetch student.'));
      return;
    });
};

Let’s deploy the function and endpoint again

sls deploy -v --stage dev --aws-profile=teacher

Now, we can test the API using cURL.

$ curl -X GET https://xxxx.execute-api.us-east-1.amazonaws.com/dev/students/827c6300-3a86-11ea-9d97-4393a018ce9f

the output will be,

{
    "experience":12,
    "id":"827c6300-3a86-11ea-9d97-4393a018ce9f",
    "email":"leelu123@gmail.com",
    "fullname":"Leeladharan M P",
    "submittedAt":1579415997743,
     "updatedAt":1579415997743
}

That’s all, Thank You.

Published:
comments powered by Disqus