Welcome to part 3 of this series. Today, we will take our journey to the next level and build the CRUD API for candidates using Serverless framework.
It is a framework that makes it very easy to build applications using AWS Lambda. It does provisioning via Cloud formations. It also scaffolds project structure and take care of deploying functions.
$ npm install serverless -g
This will install Serverless command-line on your machine. You can also use an alias sls
instead of serverless
as well.
Now, we will build application in a step by step manner.
Navigate to a convinient location on your filesystem and create a directory coding-round-evaluator
.
$ mkdir coding-round-evaluator && cd coding-round-evaluator
Once, inside the coding-round-evaluator
directory, we will scaffold our first microservice for working with candidates. This will be responsible for saving candidate details, listing candidates, and fetching a single candidate details.
$ serverless create --template aws-nodejs --path candidate-service --name candidate
This will create a directory candidate-service
with following structure.
.
├── .npmignore
├── handler.js
└── serverless.yml
Let's look at each of these three files one by one.
- .npmignore: This file is used to tell npm which files should be kept outside of the package.
- handler.js: This declares your lambda function. The created lambda function returns a 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.
Update serverless.yml to the shown below.
service: candidate
frameworkVersion: ">=1.1.0 <2.0.0"
provider:
name: aws
runtime: nodejs4.3
stage: dev
region: us-east-1
functions:
candidateSubmission:
handler: api/candidates.submit
memorySize: 128
description: Submit candidate information and starts interview process.
events:
- http:
path: candidates
method: post
Next, create a new directory api inside the candidate-service directory. Move the handler.js to the api directory. Rename handler.js to candidates.js and rename handle to submit.
'use strict';
module.exports.submit = (event, context, callback) => {
const response = {
statusCode: 200,
body: JSON.stringify({
message: 'Go Serverless v1.0! Your function executed successfully!',
input: event,
}),
};
callback(null, response);
};
To deploy the function, you can use serverless deploy
command.
$ sls deploy
Serverless: Creating Stack...
Serverless: Checking Stack create progress...
.....
Serverless: Stack create finished...
Serverless: Packaging service...
Serverless: Uploading CloudFormation file to S3...
Serverless: Uploading service .zip file to S3 (524 B)...
Serverless: Updating Stack...
Serverless: Checking Stack update progress...
....................................
Serverless: Stack update finished...
Service Information
service: candidate
stage: dev
region: us-east-1
api keys:
None
endpoints:
POST - https://05ccffiraa.execute-api.us-east-1.amazonaws.com/dev/candidates
functions:
candidate-dev-candidateSubmission
Now, POST operation of your service is available. You can use tools like cURL to make a POST request.
$ curl -X POST https://05ccffiraa.execute-api.us-east-1.amazonaws.com/dev/candidates
{"message":"Go Serverless v1.0! Your function executed successfully!", "input":{...}}
Let's write integraton with DynamoDB.
Update provider section of serverless.yml
provider:
name: aws
runtime: nodejs4.3
stage: dev
region: us-east-1
environment:
CANDIDATE_TABLE: ${self:service}-${opt:stage, self:provider.stage}
CANDIDATE_EMAIL_TABLE: "candidate-email-${opt:stage, self:provider.stage}"
iamRoleStatements:
- Effect: Allow
Action:
- dynamodb:Query
- dynamodb:Scan
- dynamodb:GetItem
- dynamodb:PutItem
Resource: "*"
Next, we will create a new resource that will create DynamoDB table as shown belowl
resources:
Resources:
CandidatesDynamoDbTable:
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.CANDIDATE_TABLE}
Now, install couple of dependencies
$ npm install --save bluebird
$ npm install --save uuid
Update the candidate/api.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 candidate because of validation errors.'));
return;
}
submitCandidateP(candidateInfo(fullname, email, experience))
.then(res => {
callback(null, {
statusCode: 200,
body: JSON.stringify({
message: `Sucessfully submitted candidate with email ${email}`,
candidateId: res.id
})
});
})
.catch(err => {
console.log(err);
callback(null, {
statusCode: 500,
body: JSON.stringify({
message: `Unable to submit candidate with email ${email}`
})
})
});
};
const submitCandidateP = candidate => {
console.log('Submitting candidate');
const candidateInfo = {
TableName: process.env.CANDIDATE_TABLE,
Item: candidate,
};
return dynamoDb.put(candidateInfo).promise()
.then(res => candidate);
};
const candidateInfo = (fullname, email, experience) => {
const timestamp = new Date().getTime();
return {
id: uuid.v1(),
fullname: fullname,
email: email,
experience: experience,
submittedAt: timestamp,
updatedAt: timestamp,
};
};
Now, you can deploy the function as shown below.
$ serverless deploy -v
This will create the DynamoDB table.
To test the API, you can use cURL again.
$ curl -H "Content-Type: application/json" -X POST -d '{"fullname":"Shekhar Gulati","email": "[email protected]", "experience":12}' https://05ccffiraa.execute-api.us-east-1.amazonaws.com/dev/candidates
The response you will receive from the API is shown below.
{
"message":"Sucessfully submitted candidate with email [email protected]",
"candidateId":"5343f0c0-f773-11e6-84ed-7bf29f824f23"
}
Define a new function in the serverless.yml as shown below.
listCandidates:
handler: api/candidates.list
memorySize: 128
description: List all candidates
events:
- http:
path: candidates
method: get
Create new function in the api/candidates.js
as shown below.
module.exports.list = (event, context, callback) => {
var params = {
TableName: process.env.CANDIDATE_TABLE,
ProjectionExpression: "id, fullname, email"
};
console.log("Scanning Candidate 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({
candidates: data.Items
})
});
}
};
dynamoDb.scan(params, onScan);
};
Deploy the function again.
$ sls deploy
Once deployed you will be able to test the API using cURL.
Define a new function in serverless.yml as shown below.
candidateDetails:
handler: api/candidates.get
events:
- http:
path: candidates/{id}
method: get
Define a new function in api/candidates.js
module.exports.get = (event, context, callback) => {
const params = {
TableName: process.env.CANDIDATE_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 candidate.'));
return;
});
};
Now, you can test the API using cURL.
curl https://05ccffiraa.execute-api.us-east-1.amazonaws.com/dev/candidates/5343f0c0-f773-11e6-84ed-7bf29f824f23
{"experience":12,"id":"5343f0c0-f773-11e6-84ed-7bf29f824f23","email":"[email protected]","fullname":"Shekhar Gulati","submittedAt":1487598537164,"updatedAt":1487598537164}
Download the jar and run locally.
sls invoke local -f function-name -p event.json
sls logs -f candidateDetails -t
In this part, you learnt about Serverless framework and how you can use it to create services. In the next part, we will learn how to enable CORS for our REST API.