Deploy a Node.js App on AWS Lambda with DynamoDB Using the Serverless Framework: A Step-by-Step Guide

Basir KhanBasir Khan
11 min read

Deploying applications in a serverless environment is becoming increasingly popular for its cost-effectiveness and ease of scaling. In this guide, you’ll learn how to deploy a Node.js application on AWS Lambda using the Serverless Framework, and connect it to DynamoDB. By the end, you’ll have a fully functional serverless app that interacts with DynamoDB, all with minimal setup and management!


1. What is the Serverless Framework?

The Serverless Framework is an open-source framework that helps developers manage serverless deployments on various cloud platforms, including AWS, Azure, and Google Cloud. It simplifies deploying serverless applications by allowing you to define resources and functions in a configuration file, eliminating the need for complex infrastructure setup.


2. Prerequisites

To follow along with this guide, ensure you have the following:

  • Node.js and npm: Install the latest version of Node.js from nodejs.org.

  • AWS Account: Set up an AWS account with access to Lambda and DynamoDB services. Sign up here.

  • Serverless Framework: Install it globally by running the command below if you haven’t already:

      npm install -g serverless
    

3. Setting Up a New Serverless Project

The first step is to create a Serverless project that will serve as the base for deploying your AWS resources and functions.

Steps:

  1. Create the Project
    Open your terminal and run the following command:

    💡
    Make sure to create your account on serverless website and validate your credentials through terminal.
     serverless
    

    This will show you several Templates. Choose 3rd option Aws / Node.js / Express Api with Dynamodb

  2. Then, select your project name in that folder. The Serverless Framework will generate your complete boilerplate code

    1. Then select create a new app to continue.

4. Next, provide an app name. A Lambda function will be created with this name

  1. Install Dependencies
    All ready some dependencies are installed through boiler plate code and we have to add some more dependencies for this project
    cd "your project name"
    npm i
    npm i crypto-js
    npm i jsonwebtoken
    npm i cors

The aws-sdk library is essential for integrating our app with AWS services.That is already installed if you selected aws + node.js+dynamodb template.


4. Configuring serverless.yml for Lambda and DynamoDB

The serverless.yml file defines your resources and functions. Here’s how to configure it to create a Lambda function and DynamoDB table.

Edit the serverless.yml file as follows:

# "org" ensures this Service is used with the correct Serverless Framework Access Key.
org: basir #change this according to your orgname
# "app" enables Serverless Framework Dashboard features and sharing them with other Services.
app: basirapp #change this to your app name
# "service" is the name of this project. This will also be added to your AWS resource names.
service: basirapp #change this according to your service name

stages:
  default:
    params:
      tableName: "basirtable" #change or create this on your aws account

provider:
  name: aws
  runtime: nodejs20.x
  region: ap-south-1
  iam:
    role:
      statements:
        - Effect: Allow
          Action:
            - dynamodb:Query
            - dynamodb:Scan
            - dynamodb:GetItem
            - dynamodb:PutItem
            - dynamodb:UpdateItem
            - dynamodb:DeleteItem
          Resource:
            - Fn::GetAtt: [UsersTable, Arn]
  environment:
    USERS_TABLE: ${param:tableName}
  httpApi:
    cors:
      allowedOrigins:
        - '*'  # Allows all origins; use specific origins for production
      allowedHeaders:
        - Content-Type
        - Authorization
        - X-Amz-Date
        - X-Api-Key
        - X-Amz-Security-Token
        - X-Requested-With
      allowedMethods:
        - GET
        - POST
        - PUT
        - DELETE
        - OPTIONS
      maxAge: 86400

functions:
  api:
    handler: handler.handler
    events:
      - httpApi:
          path: "/{proxy+}"  # Correct syntax for a catch-all route
          method: ANY

resources:
  Resources:
    UsersTable:
      Type: AWS::DynamoDB::Table
      Properties:
        AttributeDefinitions:
          - AttributeName: email
            AttributeType: S
        KeySchema:
          - AttributeName: email
            KeyType: HASH
        BillingMode: PAY_PER_REQUEST
        TableName: ${param:tableName}

Explanation:

    1. org, app, and service:

      • The org specifies the Serverless Framework organization (basir), ensuring that the service is linked with the correct Serverless Framework Access Key.

      • The app name (basirapp) enables features on the Serverless Framework Dashboard, facilitating easier management and sharing with other services.

      • The service (basirapp) is the name of this project, which will also be included in the names of the AWS resources created by this service.

      1. stages:

        • Defines a default stage with a parameter tableName, set to "basirtable". This parameter is referenced throughout the configuration to specify the DynamoDB table name.
      2. provider:

        • Specifies AWS as the cloud provider and sets the runtime to nodejs20.x.

        • The region is defined as ap-south-1.

        • Configures an IAM role with permissions to perform various DynamoDB actions (Query, Scan, GetItem, PutItem, UpdateItem, and DeleteItem) on the specified DynamoDB table.

        • The environment section defines an environment variable USERS_TABLE, which utilizes the tableName parameter value (basirtable) for use within Lambda functions.

      3. httpApi:

        • Configures CORS (Cross-Origin Resource Sharing) settings to allow requests from any origin (allowedOrigins: '*'). For production environments, it is advisable to specify allowed origins explicitly.

        • Specifies the headers and HTTP methods that are permitted in requests, along with a cache duration of maxAge: 86400 seconds (24 hours).

      4. functions:

        • Defines a Lambda function named api, with the handler implemented in handler.handler.

        • Sets up an httpApi event with a catch-all route (/{proxy+}) that allows all HTTP methods (method: ANY) to facilitate flexible request handling.

      5. resources:

        • Creates an AWS DynamoDB table named UsersTable.

        • The table's primary key is defined by the email attribute, which is of type String (S).

        • Configured with BillingMode: PAY_PER_REQUEST, allowing the table to automatically scale and bill based on the number of requests.

        • The table name is dynamically set using the tableName parameter (basirtable).


5. Writing the Lambda Handler Code

Next, create the Lambda function to save data to DynamoDB.

Steps:

  1. Update handler.js code into your project
    In the root of the project. update handler.js.

  2. Add the Following Code:

     const { DynamoDBClient } = require("@aws-sdk/client-dynamodb");
    
     const {
       DynamoDBDocumentClient,
       GetCommand,
       PutCommand,
     } = require("@aws-sdk/lib-dynamodb");
     const crypto  = require("crypto-js");
     const jwt = require("jsonwebtoken");
     const express = require("express");
     const serverless = require("serverless-http");
     const cors = require('cors');
     const app = express();
    
     const USERS_TABLE = process.env.USERS_TABLE;
     const AES_SECRET = "56snbwuy#kdhuyethj39738626rhhgfd";
     const jwtSecret = "jskhshs54w57qjhyt2652geftsrhvhagskn@medgus";
     const client = new DynamoDBClient();
     const docClient = DynamoDBDocumentClient.from(client);
    
     app.use(express.json());
     const corsOptions = {
       origin: '*', // or '*' for all origins during development
       methods: ['GET', 'POST', 'PUT', 'DELETE', 'OPTIONS'],
       allowedHeaders: ['Content-Type', 'Authorization', 'X-Amz-Date', 'X-Api-Key', 'X-Amz-Security-Token', 'X-Requested-With'],
       credentials: true,
       optionsSuccessStatus: 200 // Some legacy browsers choke on status 204
     };
    
     // Use CORS with the specified options
     app.use(cors(corsOptions));
     app.get("/",(req,res)=>{
       res.send("Hello World")
     })
     //all routes starts from here
     app.post("/register", async (req, res) => {
       const {name,email,password,clg,phone} = req.body
       const hashpass = crypto.AES.encrypt(password,AES_SECRET).toString();
       //checking the user is exist or not;
       const getParams = {
         TableName: USERS_TABLE,
         Key: {
           email: email,
         },
       };
       try{
         const data = await docClient.send(new GetCommand(getParams));
       if(data.Item){
         return res.status(400).json({error:"User already exists",success:false})
       }
     }
       catch(err){
         console.log(err)
         res.status(500).json({error:"Could not create user",success:false})
       }
       //create user
       const params = {
         TableName: USERS_TABLE,
         Item: {
           name: name,
           email: email,
           password: hashpass,
           clg: clg,
           phone: phone,
         },
       };
     try{
     let a = await docClient.send(new PutCommand(params));
     res.status(200).json({message:"User created successfully",success:true})
     }
     catch(err){
       console.log(err)
       res.status(500).json({error:"Could not create user",success:false})
     }
     })
     //login endpoint
     app.post("/login", async (req, res) => {
       try{
         const {email,password} = req.body;
         const params = {
           TableName: USERS_TABLE,
           Key: {
             email: email,
           },
         };
         try{
          let data = await docClient.send(new GetCommand(params));
           if(!data.Item){
             return res.status(404).json({error:"User not found",success:false})
           }
           else{
             const decryptpass = crypto.AES.decrypt(data.Item.password,AES_SECRET).toString(crypto.enc.Utf8);
             if(decryptpass == password){
               const token = jwt.sign({email:email},jwtSecret);
               return res.status(200).json({message:"Login Successfull",success:true,token:token})
             }
             else{
               return res.status(401).json({error:"Invalid Password",success:false})
             }
           }
         }
         catch(err){
           console.log(err)
           res.status(500).json({error:"Login Failed ! Db Error.",success:false})
         }
       }
       catch(err){
         console.log(err)
         res.status(500).json({error:"Some thing Went Wrong .Try again later!",success:false})
       }
     })
    
     app.use((req, res, next) => {
       return res.status(404).json({
         error: "Not Found",
       });
     });
     exports.handler = serverless(app);
    

Explanation:

  • This Lambda function, implemented with Node.js and Express, interacts with Amazon DynamoDB to manage user registrations and logins. Key features include:

    1. Dependencies: Utilizes @aws-sdk/client-dynamodb for database operations, crypto-js for password encryption, jsonwebtoken for creating JWT tokens, and express for routing.

    2. Environment Variables: Configures USERS_TABLE for DynamoDB, along with secret keys for AES encryption and JWT signing.

    3. Endpoints:

      • GET /: Returns "Hello World" to indicate the server is running.

      • POST /register: Handles user registration by checking for existing users, encrypting passwords, and saving user data to DynamoDB.

      • POST /login: Authenticates users by verifying credentials and returning a JWT token upon successful login.

    4. Error Handling: Includes robust error handling to provide informative responses for various scenarios, such as user existence and authentication failures.

    5. 404 Handling: Returns a 404 status for any undefined routes.

    6. AWS Lambda Integration: The exports.handler allows the Express app to run in the AWS Lambda environment.

In summary, this function provides a secure user management system with encrypted passwords and JWT-based authentication, leveraging DynamoDB for data storage.


6. Deploying the Application

With everything configured, let’s deploy your application to AWS Lambda!

Steps:

  1. Configure AWS Credentials
    Set up your AWS credentials on your local machine:

     serverless config credentials --provider aws --key YOUR_AWS_ACCESS_KEY --secret YOUR_AWS_SECRET_KEY
    
  2. Deploy the Application
    Run the following command to deploy your application:

     serverless deploy
    

    This command packages your application, uploads it to AWS, and creates the resources specified in serverless.yml.

    A DynamoDB table named "basirtable" is created through the serverless.yml configuration file, which defines the primary key as the email attribute. The table operates in PAY_PER_REQUEST billing mode, ensuring efficient scaling and cost management based on actual usage.

    Also check lamda our function is deployed successfully.


7. Testing the Deployment

After deployment, test the endpoint to confirm that it works as expected.

  1. Get the Endpoint URL
    The deployment process will output an endpoint URL in the terminal.

  2. Send a Request to the URL
    Use a tool like curl or Postman to send a GET request to the endpoint:

     GET https://x60hefyye3.execute-api.ap-south-1.amazonaws.com
    

    Sends a post request to create an account to “/register” end point through postman.

    Feel free to check another end point as well in my case all end point is working fine.


8. Verifying DynamoDB Data

Now, let’s confirm that the data was saved in DynamoDB.

  1. Go to the DynamoDB Console
    In your AWS Management Console, open DynamoDB.

  2. Check the Table
    Find your table (MyTable), and open it to view items. You should see the item created by your Lambda function.


9. Wrapping Up

Congratulations! 🎉 You’ve successfully deployed a Node.js app to AWS Lambda using the Serverless Framework and integrated it with DynamoDB. This setup demonstrates the power of serverless architecture, reducing costs and simplifying scaling while allowing you to quickly deploy applications.

By following this guide, you now have a foundational setup you can build upon, expanding your application’s features or integrating more AWS services. Enjoy building with serverless, and keep experimenting to leverage AWS Lambda and DynamoDB for even more advanced use cases!


10. Bonous Section

In this section, we will connect the Lambda function to our frontend via a REST API, allowing seamless interaction between the backend and frontend components. This integration will enable us to handle requests efficiently, enhancing the application's responsiveness and user experience. Let's proceed with setting up the API endpoint to establish this connection.

💡
I will create a frontend for this integration. You can either clone the provided frontend repository or use your own—both approaches will follow similar steps. We’ll update the API endpoint accordingly to connect it with the Lambda function and ensure a smooth data flow between the frontend and backend. Let's proceed with setting up the frontend and linking it to the updated API endpoint.

1. Clone the github repo .

💡
If you don’t have Git installed on your machine, feel free to download the repository as a ZIP file. Then, extract it, and all the remaining steps will be the same.
git clone https://github.com/BasirKhan418/basirapp-serverless-frontend.git
cd basirapp-serverless-frontend
npm i
  1. Create a .env file and add the following content. Feel free to update the value of VITE_BACKEND_URL with the URL of your own Lambda function deployment or API gateway:

     VITE_BACKEND_URL=<your_lambda_function_url>
    

  2. Build your project / frontend.

npm run build
  1. After a successful build, a dist folder will be generated. This folder contains the production-ready files, which we need to upload to S3 to host the frontend application.

  2. Create an S3 bucket with your desired name. Uncheck the option to block all public access, leave all other settings as default, and click Create bucket to proceed.

  3. Click on recently created bucket go to permissions and click on edit bucket policy and paste the content below of it and make sure to add your s3 arn.

     {
         "Version": "2012-10-17",
         "Statement": [
             {
                 "Sid": "Statement1",
                 "Principal": "*",
                 "Effect": "Allow",
                 "Action": [
                     "s3:GetObject"
                 ],
                 "Resource": "your arn/*"
             }
         ]
     }
    
  4. Upload you dist folder contents to s3

  5. Navigate to the Properties tab, scroll down to the Static website hosting section at the bottom of the page, and click Edit. Enable static website hosting, enter index.html in the Index document field, and click Save changes to apply.

  6. 🎉 Hooray! Your frontend has been deployed successfully.

  7. Open the generated link in a new tab, try registering, and check if the data is being saved in DynamoDB.

Conclusion:

By following this guide, titled "Deploy a Node.js App on AWS Lambda with DynamoDB Using Serverless Framework: Step-by-Step Guide," you've successfully deployed a full-stack, serverless registration and login system using AWS. Your Node.js backend now operates on AWS Lambda, with DynamoDB handling data storage, ensuring both security and scalability. Additionally, hosting the frontend on S3 offers a cost-effective, high-performance solution for web access.

This setup not only provides a robust foundation for authentication workflows but also streamlines application management by leveraging AWS services. With this approach, you're well-equipped to scale effortlessly, adding new features or integrating additional AWS services as needed.

In summary, this beginner-friendly, step-by-step guide has equipped you with the knowledge to deploy a fully functional serverless application in the cloud, perfect for developers looking to learn AWS Lambda and DynamoDB integration!

0
Subscribe to my newsletter

Read articles from Basir Khan directly inside your inbox. Subscribe to the newsletter, and don't miss out.

Written by

Basir Khan
Basir Khan

Full-Stack Developer | MERN + Next.js | DevOps & Cloud Enthusiast I specialize in building dynamic web applications using the MERN stack and Next.js. Currently exploring DevOps and cloud technologies to streamline workflows, automate deployments, and optimize cloud infrastructure for scalable, efficient solutions.