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.



