DEV Community

Deepak Sharma
Deepak Sharma

Posted on • Originally published at Medium

Build a highly scalable Serverless CRUD Microservice with AWS Lambda and the Serverless Framework

In today’s cloud-native world, serverless architecture has become increasingly popular for building scalable and cost-effective applications. This blog post will guide you through creating a production-ready serverless CRUD (Create, Read, Update, Delete) microservice using AWS Lambda, DynamoDB, and the Serverless Framework.

Agenda:
Create a fully functional REST API that can:

  • Create new items
  • Retrieve items (both individual and list)
  • Update existing items
  • Delete items

Prerequisites:

  • Node.js installed on your machine
  • AWS account with appropriate permissions
  • Basic understanding of JavaScript/Node.js
  • AWS CLI installed and configured
  • Serverless Framework CLI installed

Architecture:
Below is the high level architectural diagram of the implementation.

  • AWS Lambda functions for each CRUD operation
  • DynamoDB for data storage
  • API Gateway for HTTP endpoints
  • Proper IAM roles and permissions
  • Cloudwatch logs to access the logs

Image description

Project Setup:
First, let’s create our project structure and install necessary dependencies:

mkdir serverless-crud-ms
cd serverless-crud-ms
npm init -y
npm install aws-sdk uuid
Enter fullscreen mode Exit fullscreen mode

Once the above dependencies are installed, we proceed with the project structure creation which should look like below:

.
├── serverless.yml
├── handlers
│   ├── create.js
│   ├── get.js
│   ├── update.js
│   └── delete.js
└── package.json
Enter fullscreen mode Exit fullscreen mode

Project Structure:

  • serverless.yml: Configuration for functions, resources, and permissions. It is the backbone of the application, defining our infrastructure as code.
  • handlers/: Contains Lambda function handlers
  • package.json: Project dependencies

Implementing CRUD Operations:
For each database operation a respective handler (.js) file is created under the handler directory. We will go through each one of them in corresponding sections. The names of the files are create.js, get.js, update.js, delete.js

Create Operation
To start with, a new file “create.js” is created for the POST operation, where a new item is inserted in the dynamo db table. The api uses the aws sdk client kit to insert records in dynamodb table. The operation performs the below:

  • Input validation
  • Error handling
  • CORS support
  • Unique ID generation
  • Timestamps for created/updated items Below is the code snippet for reference:
const AWS = require('aws-sdk');
const { v4: uuidv4 } = require('uuid');

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

module.exports.create = async (event) => {
    try {
        const timestamp = new Date().getTime();
        const data = JSON.parse(event.body);

        if (!data.name || !data.description) {
            return {
                statusCode: 400,
                headers: {
                    'Content-Type': 'application/json',
                    'Access-Control-Allow-Origin': '*'
                },
                body: JSON.stringify({
                    message: 'Missing required fields'
                })
            };
        }

        const params = {
            TableName: process.env.DYNAMODB_TABLE,
            Item: {
                id: uuidv4(),
                name: data.name,
                description: data.description,
                createdAt: timestamp,
                updatedAt: timestamp
            }
        };

        await dynamoDb.put(params).promise();

        return {
            statusCode: 201,
            headers: {
                'Content-Type': 'application/json',
                'Access-Control-Allow-Origin': '*'
            },
            body: JSON.stringify(params.Item)
        };
    } catch (error) {
        console.error(error);
        return {
            statusCode: error.statusCode || 500,
            headers: {
                'Content-Type': 'application/json',
                'Access-Control-Allow-Origin': '*'
            },
            body: JSON.stringify({
                message: error.message || 'Internal server error'
            })
        };
    }
};
Enter fullscreen mode Exit fullscreen mode

Read Operation
To start with, a new file “get.js” is created for the GET operation, which fetches all the item records or a specific item record from the dynamo db table.

Below is the code snippet for reference:

const AWS = require('aws-sdk');
const dynamoDb = new AWS.DynamoDB.DocumentClient();

module.exports.getAll = async (event) => {
    try {
        const params = {
            TableName: process.env.DYNAMODB_TABLE
        };

        const result = await dynamoDb.scan(params).promise();

        return {
            statusCode: 200,
            headers: {
                'Content-Type': 'application/json',
                'Access-Control-Allow-Origin': '*'
            },
            body: JSON.stringify(result.Items)
        };
    } catch (error) {
        console.error(error);
        return {
            statusCode: error.statusCode || 500,
            headers: {
                'Content-Type': 'application/json',
                'Access-Control-Allow-Origin': '*'
            },
            body: JSON.stringify({
                message: error.message || 'Internal server error'
            })
        };
    }
};

module.exports.getOne = async (event) => {
    try {
        const params = {
            TableName: process.env.DYNAMODB_TABLE,
            Key: {
                id: event.pathParameters.id
            }
        };

        const result = await dynamoDb.get(params).promise();

        if (!result.Item) {
            return {
                statusCode: 404,
                headers: {
                    'Content-Type': 'application/json',
                    'Access-Control-Allow-Origin': '*'
                },
                body: JSON.stringify({
                    message: 'Item not found'
                })
            };
        }

        return {
            statusCode: 200,
            headers: {
                'Content-Type': 'application/json',
                'Access-Control-Allow-Origin': '*'
            },
            body: JSON.stringify(result.Item)
        };
    } catch (error) {
        console.error(error);
        return {
            statusCode: error.statusCode || 500,
            headers: {
                'Content-Type': 'application/json',
                'Access-Control-Allow-Origin': '*'
            },
            body: JSON.stringify({
                message: error.message || 'Internal server error'
            })
        };
    }
};
Enter fullscreen mode Exit fullscreen mode

Update Operation
To start with, a new file “update.js” is created for the PUT operation, which updates an existing item record in the dynamo db table.

Below is the code snippet for reference:

const AWS = require('aws-sdk');
const dynamoDb = new AWS.DynamoDB.DocumentClient();

module.exports.update = async (event) => {
    try {
        const timestamp = new Date().getTime();
        const data = JSON.parse(event.body);

        if (!data.name || !data.description) {
            return {
                statusCode: 400,
                headers: {
                    'Content-Type': 'application/json',
                    'Access-Control-Allow-Origin': '*'
                },
                body: JSON.stringify({
                    message: 'Missing required fields'
                })
            };
        }

        const params = {
            TableName: process.env.DYNAMODB_TABLE,
            Key: {
                id: event.pathParameters.id
            },
            ExpressionAttributeNames: {
                '#item_name': 'name'
            },
            ExpressionAttributeValues: {
                ':name': data.name,
                ':description': data.description,
                ':updatedAt': timestamp
            },
            UpdateExpression: 'SET #item_name = :name, description = :description, updatedAt = :updatedAt',
            ReturnValues: 'ALL_NEW'
        };

        const result = await dynamoDb.update(params).promise();

        return {
            statusCode: 200,
            headers: {
                'Content-Type': 'application/json',
                'Access-Control-Allow-Origin': '*'
            },
            body: JSON.stringify(result.Attributes)
        };
    } catch (error) {
        console.error(error);
        return {
            statusCode: error.statusCode || 500,
            headers: {
                'Content-Type': 'application/json',
                'Access-Control-Allow-Origin': '*'
            },
            body: JSON.stringify({
                message: error.message || 'Internal server error'
            })
        };
    }
};
Enter fullscreen mode Exit fullscreen mode

Delete Operation
To start with, a new file “delete.js” is created for the DEL operation, which deletes a specific item record from the dynamo db table whose id is provided as input.

Below is the code snippet for reference:

const AWS = require('aws-sdk');
const dynamoDb = new AWS.DynamoDB.DocumentClient();

module.exports.delete = async (event) => {
    try {
        const params = {
            TableName: process.env.DYNAMODB_TABLE,
            Key: {
                id: event.pathParameters.id
            }
        };

        await dynamoDb.delete(params).promise();

        return {
            statusCode: 204,
            headers: {
                'Content-Type': 'application/json',
                'Access-Control-Allow-Origin': '*'
            },
            body: ''
        };
    } catch (error) {
        console.error(error);
        return {
            statusCode: error.statusCode || 500,
            headers: {
                'Content-Type': 'application/json',
                'Access-Control-Allow-Origin': '*'
            },
            body: JSON.stringify({
                message: error.message || 'Internal server error'
            })
        };
    }
};
Enter fullscreen mode Exit fullscreen mode

serverless.yml
The serverless.yml file contains the configuration of runtime, handler functions, aws region, and dynamoDB resources. Below is how my file looks like:

service: serverless-crud-ms

provider:
  name: aws
  runtime: nodejs18.x
  stage: ${opt:stage, 'dev'}
  region: eu-central-1
  environment:
    DYNAMODB_TABLE: ${self:service}-${self:provider.stage}
  iamRoleStatements:
    - Effect: Allow
      Action:
        - dynamodb:Query
        - dynamodb:Scan
        - dynamodb:GetItem
        - dynamodb:PutItem
        - dynamodb:UpdateItem
        - dynamodb:DeleteItem
      Resource: "arn:aws:dynamodb:${self:provider.region}:*:table/${self:provider.environment.DYNAMODB_TABLE}"

functions:
  create:
    handler: handlers/create.create
    events:
      - http:
          path: items
          method: post
          cors: true

  getAll:
    handler: handlers/get.getAll
    events:
      - http:
          path: items
          method: get
          cors: true

  getOne:
    handler: handlers/get.getOne
    events:
      - http:
          path: items/{id}
          method: get
          cors: true

  update:
    handler: handlers/update.update
    events:
      - http:
          path: items/{id}
          method: put
          cors: true

  delete:
    handler: handlers/delete.delete
    events:
      - http:
          path: items/{id}
          method: delete
          cors: true

resources:
  Resources:
    ItemsTable:
      Type: AWS::DynamoDB::Table
      Properties:
        TableName: ${self:provider.environment.DYNAMODB_TABLE}
        AttributeDefinitions:
          - AttributeName: id
            AttributeType: S
        KeySchema:
          - AttributeName: id
            KeyType: HASH
        BillingMode: PAY_PER_REQUEST
Enter fullscreen mode Exit fullscreen mode

Deployment:
Deploy the service using below commands:

# Configure AWS credentials
aws configure

# Deploy to AWS
serverless deploy
Enter fullscreen mode Exit fullscreen mode

Note: You may encounter below error on deployment:
serverless.ps1 cannot be loaded. The file ..\npm\serverless.ps1 is not digitally signed.
To resolve this error, you may execute the below command and retry deployment:

Set-ExecutionPolicy -Scope Process -ExecutionPolicy Bypass
Enter fullscreen mode Exit fullscreen mode

Once the deployment is successful, the API endpoints will be generated in the format like below:

  • Create new items (POST /items)
  • Retrieve all items (GET /items)
  • Get specific items by ID (GET /items/{id})
  • Update existing items (PUT /items/{id})
  • Delete items (DELETE /items/{id})
endpoints:
  POST - https://xxxxx.execute-api.eu-central-1.amazonaws.com/dev/items
  GET - https://xxxxx.execute-api.eu-central-1.amazonaws.com/dev/items
  GET - https://xxxxx.execute-api.eu-central-1.amazonaws.com/dev/items/{id}
  PUT - https://xxxxx.execute-api.eu-central-1.amazonaws.com/dev/items/{id}
  DELETE - https://xxxxx.execute-api.eu-central-1.amazonaws.com/dev/items/{id}
Enter fullscreen mode Exit fullscreen mode

Testing the API
Use curl or Postman to test your endpoints:

# Create an item
curl -X POST https://your-api-url/dev/items \
  -H "Content-Type: application/json" \
  -d '{"name": "Test Item 1", "description": "This is a test item 1"}'

# Get all items
curl https://your-api-url/dev/items

# Get one item
curl https://your-api-url/dev/items/{id}

# Update an item
curl -X PUT https://your-api-url/dev/items/{id} \
  -H "Content-Type: application/json" \
  -d '{"name": "Updated Item 1", "description": "This is an updated item 1"}'

# Delete an item
curl -X DELETE https://your-api-url/dev/items/{id}
Enter fullscreen mode Exit fullscreen mode

Cleanup
To remove all deployed resources:

serverless remove
Enter fullscreen mode Exit fullscreen mode

This command will (check your AWS Console):

  • Delete the Lambda functions
  • Remove the API Gateway endpoints
  • Delete the DynamoDB table
  • Remove the IAM roles and policies
  • Clean up any CloudWatch log groups
  • Remove any other AWS resources that were created as part of your service

Conclusion
We’ve built a production-ready serverless CRUD microservice that’s scalable, maintainable, and follows best practices. This architecture can serve as a foundation for more complex applications, handling millions of requests while maintaining cost efficiency through the serverless model.

Top comments (0)