How to deploy Node.js app to AWS Lambda

Deploying Node.js applications to AWS Lambda provides serverless compute with automatic scaling, pay-per-execution pricing, and integration with AWS services. With over 12 years of Node.js experience since 2014 and as the creator of CoreUI, I’ve deployed numerous Lambda functions for production services. AWS Lambda runs Node.js code in response to events from API Gateway, S3, DynamoDB, and other triggers with zero server management. This approach offers massive scalability, cost efficiency for variable workloads, and integration with the broader AWS ecosystem.

Use AWS Lambda with API Gateway for HTTP endpoints and Serverless Framework for simplified deployment and management.

Basic Lambda function:

// handler.js
exports.handler = async (event, context) => {
  console.log('Event:', JSON.stringify(event, null, 2))

  return {
    statusCode: 200,
    headers: {
      'Content-Type': 'application/json',
      'Access-Control-Allow-Origin': '*'
    },
    body: JSON.stringify({
      message: 'Hello from AWS Lambda!',
      timestamp: new Date().toISOString(),
      requestId: context.requestId
    })
  }
}

Serverless Framework configuration:

# serverless.yml
service: my-nodejs-api

provider:
  name: aws
  runtime: nodejs20.x
  region: us-east-1
  stage: ${opt:stage, 'dev'}
  memorySize: 512
  timeout: 10
  environment:
    NODE_ENV: production
    TABLE_NAME: ${self:custom.tableName}
  iam:
    role:
      statements:
        - Effect: Allow
          Action:
            - dynamodb:Query
            - dynamodb:Scan
            - dynamodb:GetItem
            - dynamodb:PutItem
          Resource: "arn:aws:dynamodb:${self:provider.region}:*:table/${self:custom.tableName}"

custom:
  tableName: users-table-${self:provider.stage}

functions:
  hello:
    handler: handler.hello
    events:
      - http:
          path: hello
          method: get
          cors: true

  getUser:
    handler: handler.getUser
    events:
      - http:
          path: users/{id}
          method: get
          cors: true

  createUser:
    handler: handler.createUser
    events:
      - http:
          path: users
          method: post
          cors: true

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

REST API handlers:

// handler.js
const AWS = require('aws-sdk')
const dynamoDB = new AWS.DynamoDB.DocumentClient()

const headers = {
  'Content-Type': 'application/json',
  'Access-Control-Allow-Origin': '*',
  'Access-Control-Allow-Credentials': true
}

exports.hello = async (event, context) => {
  return {
    statusCode: 200,
    headers,
    body: JSON.stringify({ message: 'Hello World' })
  }
}

exports.getUser = async (event, context) => {
  const { id } = event.pathParameters

  try {
    const result = await dynamoDB.get({
      TableName: process.env.TABLE_NAME,
      Key: { id }
    }).promise()

    if (!result.Item) {
      return {
        statusCode: 404,
        headers,
        body: JSON.stringify({ error: 'User not found' })
      }
    }

    return {
      statusCode: 200,
      headers,
      body: JSON.stringify(result.Item)
    }
  } catch (error) {
    console.error('Error:', error)
    return {
      statusCode: 500,
      headers,
      body: JSON.stringify({ error: 'Internal server error' })
    }
  }
}

exports.createUser = async (event, context) => {
  const data = JSON.parse(event.body)

  const user = {
    id: Date.now().toString(),
    name: data.name,
    email: data.email,
    createdAt: new Date().toISOString()
  }

  try {
    await dynamoDB.put({
      TableName: process.env.TABLE_NAME,
      Item: user
    }).promise()

    return {
      statusCode: 201,
      headers,
      body: JSON.stringify(user)
    }
  } catch (error) {
    console.error('Error:', error)
    return {
      statusCode: 500,
      headers,
      body: JSON.stringify({ error: error.message })
    }
  }
}

Environment variables:

# serverless.yml
provider:
  environment:
    NODE_ENV: ${self:provider.stage}
    API_KEY: ${env:API_KEY}
    DATABASE_URL: ${env:DATABASE_URL}
    JWT_SECRET: ${env:JWT_SECRET}

# .env file (local development)
API_KEY=your-api-key
DATABASE_URL=your-database-url
JWT_SECRET=your-secret

Package.json:

{
  "name": "aws-lambda-nodejs",
  "version": "1.0.0",
  "scripts": {
    "deploy": "serverless deploy",
    "deploy:prod": "serverless deploy --stage prod",
    "invoke": "serverless invoke local -f hello",
    "logs": "serverless logs -f hello -t",
    "remove": "serverless remove"
  },
  "dependencies": {
    "aws-sdk": "^2.1505.0"
  },
  "devDependencies": {
    "serverless": "^3.38.0",
    "serverless-offline": "^13.3.0"
  }
}

Local development with Serverless Offline:

# serverless.yml
plugins:
  - serverless-offline

custom:
  serverless-offline:
    httpPort: 3000
    lambdaPort: 3002

# Run locally
# npm install serverless-offline
# serverless offline

Authentication with JWT:

// auth.js
const jwt = require('jsonwebtoken')

exports.generateToken = (user) => {
  return jwt.sign(
    { id: user.id, email: user.email },
    process.env.JWT_SECRET,
    { expiresIn: '24h' }
  )
}

exports.verifyToken = (token) => {
  try {
    return jwt.verify(token, process.env.JWT_SECRET)
  } catch (error) {
    return null
  }
}

// middleware.js
const { verifyToken } = require('./auth')

exports.authenticate = async (event, context) => {
  const token = event.headers.Authorization?.replace('Bearer ', '')

  if (!token) {
    return {
      statusCode: 401,
      body: JSON.stringify({ error: 'No token provided' })
    }
  }

  const decoded = verifyToken(token)

  if (!decoded) {
    return {
      statusCode: 401,
      body: JSON.stringify({ error: 'Invalid token' })
    }
  }

  // Add user to event
  event.user = decoded
  return null // Continue to handler
}

// Protected handler
const { authenticate } = require('./middleware')

exports.protectedHandler = async (event, context) => {
  const authError = await authenticate(event, context)
  if (authError) return authError

  return {
    statusCode: 200,
    body: JSON.stringify({
      message: 'Protected data',
      user: event.user
    })
  }
}

Database connection pooling:

// db.js
const { Client } = require('pg')

let client

async function getClient() {
  if (!client || client._ending) {
    client = new Client({
      connectionString: process.env.DATABASE_URL,
      ssl: { rejectUnauthorized: false }
    })
    await client.connect()
  }
  return client
}

exports.query = async (sql, params) => {
  const client = await getClient()
  return client.query(sql, params)
}

// handler.js
const db = require('./db')

exports.getUsers = async (event, context) => {
  try {
    const result = await db.query('SELECT * FROM users LIMIT 10')

    return {
      statusCode: 200,
      body: JSON.stringify({ users: result.rows })
    }
  } catch (error) {
    return {
      statusCode: 500,
      body: JSON.stringify({ error: error.message })
    }
  }
}

S3 event trigger:

// s3-handler.js
const AWS = require('aws-sdk')
const s3 = new AWS.S3()

exports.processImage = async (event, context) => {
  console.log('S3 Event:', JSON.stringify(event, null, 2))

  for (const record of event.Records) {
    const bucket = record.s3.bucket.name
    const key = decodeURIComponent(record.s3.object.key.replace(/\+/g, ' '))

    console.log(`Processing file: ${bucket}/${key}`)

    try {
      // Get file from S3
      const file = await s3.getObject({
        Bucket: bucket,
        Key: key
      }).promise()

      // Process file
      console.log(`File size: ${file.Body.length} bytes`)

      // Your processing logic here

      return {
        statusCode: 200,
        body: JSON.stringify({ message: 'File processed successfully' })
      }
    } catch (error) {
      console.error('Error processing file:', error)
      throw error
    }
  }
}

// serverless.yml
functions:
  processImage:
    handler: s3-handler.processImage
    events:
      - s3:
          bucket: my-image-bucket
          event: s3:ObjectCreated:*
          rules:
            - prefix: uploads/
            - suffix: .jpg

Deployment:

# Install Serverless Framework
npm install -g serverless

# Configure AWS credentials
serverless config credentials --provider aws --key YOUR_KEY --secret YOUR_SECRET

# Deploy to AWS
serverless deploy

# Deploy specific stage
serverless deploy --stage prod

# Invoke function
serverless invoke -f hello

# View logs
serverless logs -f hello -t

# Remove service
serverless remove

CI/CD with GitHub Actions:

# .github/workflows/deploy.yml
name: Deploy to AWS Lambda

on:
  push:
    branches: [main]

jobs:
  deploy:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v3

      - name: Setup Node.js
        uses: actions/setup-node@v3
        with:
          node-version: '20'

      - name: Install dependencies
        run: npm ci

      - name: Deploy to AWS
        env:
          AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }}
          AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
        run: serverless deploy --stage prod

Best Practice Note

Use Serverless Framework for simplified deployment and infrastructure management. Reuse database connections between invocations for better performance. Set appropriate memory and timeout limits for functions. Use environment variables for configuration. Implement proper error handling and logging with CloudWatch. Use Lambda layers for shared dependencies. Enable X-Ray tracing for debugging. Monitor costs with AWS Cost Explorer. Use VPC for database access when needed. This is how we deploy to AWS Lambda—Serverless Framework for infrastructure as code, proper connection pooling, and monitoring ensuring scalable, cost-effective serverless Node.js backends.


Speed up your responsive apps and websites with fully-featured, ready-to-use open-source admin panel templates—free to use and built for efficiency.


About the Author

Subscribe to our newsletter
Get early information about new products, product updates and blog posts.
How to migrate CoreUI React Templates to Vite
How to migrate CoreUI React Templates to Vite

How to Manage Date and Time in Specific Timezones Using JavaScript
How to Manage Date and Time in Specific Timezones Using JavaScript

CSS Selector for Parent Element
CSS Selector for Parent Element

JavaScript Template Literals: Complete Developer Guide
JavaScript Template Literals: Complete Developer Guide

Answers by CoreUI Core Team