Serverless framework is a command line tool that can be used to build and deploy AWS serverless microservices using AWS Lambda. It offers various project templates to start with. You can configure the serverless.yml file to configure the AWS resources you want to provision during deployment of microservice.
* NodeJS 14.x
* Visual Studio Code
* Serverless Framework CLI
* AWS account
This tutorial will help you to create a serverless microservice for managing the Todo actions using expressjs and DynamoDb.
-
Run the following command to install the latest version of
serverlessCommand line tool.npm install -g serverless
-
After the CLI has been successfully installed, run the following command to create a new project that uses Expressjs and DynamoDB for creating serverless Lambda project.
serverless --name todo-api
-
For the
What org do you want to add this service to?option choose and organization name or select[Skip]. -
For the
Do you want to deploy your project? (Y/n)option enternto specifyNo. -
Switch to the project directory using the following command.
cd todo-api -
You need to update the
serverless.ymlto specify the AWS resources such as Lambda and DynamoDB. Update thecustomsection with the following code to specify the DynamoDb table name asTodos.custom: tableName: 'Todos'
-
In the provider section, update the environment variable name to
TODO_TABLE.provider: name: aws runtime: nodejs12.x lambdaHashingVersion: '20201221' iam: role: statements: - Effect: Allow Action: - dynamodb:Query - dynamodb:Scan - dynamodb:GetItem - dynamodb:PutItem - dynamodb:UpdateItem - dynamodb:DeleteItem Resource: - Fn::GetAtt: [ TodoTable, Arn ] environment: TODO_TABLE: ${self:custom.tableName}
-
Update the
functionssection to change the Lambda api name astodo-api.functions: todo-api: handler: handler.handler events: - httpApi: '*'
-
Also update the configuration for DynamoDb by updating the
resourcessection. It will create a DynamoDb resource with the table name specified in the above custom section. The table will have a partition keyEmailand a Sort keyId.resources: Resources: TodoTable: Type: AWS::DynamoDB::Table Properties: AttributeDefinitions: - AttributeName: Email AttributeType: S - AttributeName: Id AttributeType: S KeySchema: - AttributeName: Email KeyType: HASH - AttributeName: Id KeyType: RANGE BillingMode: PAY_PER_REQUEST TableName: ${self:custom.tableName}
-
In the project you need to install some npm dependencies such as
@aws-sk/client-dynamodb,@aws-sdk/util-dynamodb, anduuid.npm install -S @aws-sk/client-dynamodb @aws-sdk/util-dynamodb uuid
-
Create a new folder in the project and name it as
helpers. Add a new file calledddbclient.jsto it and add the following code to it.const { DynamoDBClient } = require("@aws-sdk/client-dynamodb"); const ddbClient = new DynamoDBClient({ region: 'us-east-1' }); module.exports = { ddbClient };
-
Also add another javascript file called
todo-helper.jsin thehelpersfolder and add the following CRUD operations code to it.const { ddbClient } = require('./ddbclient'); const { QueryCommand, PutItemCommand, GetItemCommand, ScanCommand } = require('@aws-sdk/client-dynamodb'); const { marshall, unmarshall } = require('@aws-sdk/util-dynamodb'); class TodoService { constructor(tableName) { this.TABLENAME = tableName } // addTodo(todo) { // let params = { // TableName: this.TABLENAME, // Item: { // Email: { 'S': todo.Email }, //PK // Id: { 'S': todo.Id }, //SK // Title: { 'S': todo.Title}, // IsCompleted: { 'BOOL': todo.IsCompleted } // } // } // return ddbClient.send(new PutItemCommand(params)) // } addTodo(todo) { let params = { TableName: this.TABLENAME, Item: marshall(todo) } return ddbClient.send(new PutItemCommand(params)) } async getTodosByEmail(email) { var params = { TableName: this.TABLENAME, KeyConditionExpression: "Email = :email", ExpressionAttributeValues: { ":email": { 'S': email } } }; let result = await ddbClient.send(new QueryCommand(params)) .catch(err => Promise.reject(err)); let todos= []; result.Items.forEach((item)=> todos.push(unmarshall(item))); return Promise.resolve(todos) } async getTodo(email, id) { var params = { TableName: this.TABLENAME, Key: marshall({ Email: email, Id: id }) }; let result = await ddbClient.send(new GetItemCommand(params)) .catch(err => Promise.reject(err)); return Promise.resolve(unmarshall(result.Item)) } async getAll() { let params = { TableName: this.TABLENAME, } let result = await ddbClient.send(new ScanCommand(params)) .catch(err => Promise.reject(err)); let todos = []; result.Items.forEach((item) => todos.push(unmarshall(item))); return Promise.resolve(todos) } } module.exports = { TodoService }
-
Create another folder in the project root folder named
routes. Define the express routes configurations inside this folder. Create a new javascript file namedtodo-routes.jsin theroutesfolder and add the following code to it.const { Router } = require('express'); const uuid = require('uuid'); const { TodoService } = require("../helpers/todo-helper"); var router = Router(); const todoSvc = new TodoService(process.env.TODO_TABLE); //Add a todo item //POST /todos router.post("/", async function (req, res) { const todo = req.body; todo.Id = uuid.v4(); if (typeof todo.Email !== "string") { res.status(400).json({ error: '"Email" must be a string' }); } else if (typeof todo.Title !== "string") { res.status(400).json({ error: '"Title" must be a string' }); } else if (typeof todo.IsCompleted != "boolean") res.status(400).json({ error: '"IsCompleted" must be a boolean' }) let result = await todoSvc.addTodo(todo) .catch(err => { console.log('Error received in catch:', err); res.status(500).json(`Error:Unable to insert todo item`); }); if (result) { res.status(201).json(`Todo item created with id '${todo.Id}'`) } }); //Get a single todo item //GET /todos/user/:email/id:/:id router.get("/user/:email/id/:id", async function (req, res) { let result = await todoSvc.getTodo(req.params.email, req.params.id) .catch(err => { console.log(err); res.status(500).json({ error: 'Unable to get todo item' }); }); if (result) { res.status(200).json(result); } else { res.status(404).json({ error: 'Could not find todo item' }); } }); //Get todo items for a single user //GET /todos/user/:email router.get("/user/:email", async function (req, res) { let result = await todoSvc.getTodosByEmail(req.params.email) .catch(err => { console.log(err); res.status(500).json({ error: 'Unable to get todo items' }); }); if (result) { res.status(200).json(result); } else { res.status(404).json({ error: `Could not get todo items for ${req.params.email}` }); } }); //Get all todos for all users //GET /todos router.get("/", async function (req, res) { let result = await todoSvc.getAll() .catch(err => { console.log(err); res.status(500).json({ error: 'Unable to get todo items' }); }); res.status(200).json(result); }) module.exports = router;
-
Update the
handler.jsfile in the root folder with the following code. This will configure the routes for the microservices api.const express = require("express"); const serverless = require("serverless-http"); const app = express(); app.use(express.json()); app.use("/todos", require('./routes/todo-routes')) //Error handler middleware app.use((req, res, next) => { return res.status(404).json({ error: "Not Found", }); }); module.exports.handler = serverless(app);
-
You are now ready to deploy the project to AWS. Run the following command to deploy the serverless application in AWS Lambda. It also create the dependent resources such as
IAM role,API gatewayS3 bucketandDynamoDB.serverless deploy
-
After the deployment is successfully completed, you can see the http endpoint of your API gateway to invoke the microservice.
Serverless: Packaging service... Serverless: Excluding development dependencies... Serverless: Ensuring that deployment bucket exists Serverless: Uploading CloudFormation file to S3... Serverless: Uploading artifacts... Serverless: Uploading service todo-api.zip file to S3 (3.02 MB)... Serverless: Validating template... Serverless: Updating Stack... Serverless: Checking Stack update progress... ......... Serverless: Stack update finished... Service Information service: todo-api stage: dev region: us-east-1 stack: todo-api-dev resources: 12 api keys: None endpoints: ANY - https://g6pty9m55f.execute-api.us-east-1.amazonaws.com functions: todo-api: todo-api-dev-todo-api layers: None Serverless: Removing old service artifacts from S3...
- Open the API gateway console and select the
dev-todo-apito navigate to the API dashboard. - Select
CORSoption from theDevelopsection on the left side navigation bar. - In the
Cross-Origin Resource Sharingpage, click on theConfigurebutton. This will open the CORS policy editor. - Type the domain url of consumer applications in the
Access-Control-Allow-Origintext box and clickAddbutton. You can add more domains by typing and clicking on add button. To allow requests from all domains specify*for the domain value. - For
Access-Control-Allow-Headers, specify*as the value and clickAddbutton. - For
Access-Control-Allow-Methods, specifyGETmethod and click onAddbutton. - Other values you can leave as default and click on the
Savebutton.
