Building Serverless APIs with AWS Lambda and API Gateway — Guide
In this tutorial, you'll learn about Building Serverless APIs with AWS Lambda and API Gateway. We cover key concepts, practical examples, and best practices to help you understand and apply this topic effectively.
Serverless APIs use AWS Lambda for compute and API Gateway for HTTP endpoints, enabling pay-per-request pricing, automatic scaling, and zero server management for building cost-effective REST APIs.
What You'll Learn
You will learn to build Serverless REST APIs with AWS Lambda and API Gateway, write Lambda handlers in Node.js and Python, configure API Gateway routes and authentication, manage environment variables, and monitor Serverless applications.
Why Serverless APIs Matter
Serverless eliminates server management, scales from zero to thousands of requests instantly, and charges only for actual usage. AWS Lambda costs nothing when idle, making it ideal for variable traffic patterns. Companies using Serverless report 60 percent lower operational costs and 40 percent faster time to market.
Real-World Use
DodaTech runs several Serverless APIs. Doda Browser analytics endpoint processes millions of events through Lambda, DodaZIP uses Serverless functions for file metadata extraction, and Durga Antivirus Pro threat report submission runs on Lambda with API Gateway for automatic scaling during threat outbreaks.
Serverless APIs Learning Path
flowchart LR
A[Cloud Computing Basics] --> B[Lambda Concepts]
B --> C[Function Handlers]
C --> D[API Gateway Integration]
D --> E[Environment & Secrets]
E --> F[Deployment & CI/CD]
F --> G[Monitoring & Debugging]
B:::current
classDef current fill:#f90,color:#fff,stroke:#333,stroke-width:2px
Prerequisites
Understand RESTful Api Design Best Practices and basic Cloud Computing Basics. An AWS account is required for deployment. Familiarity with JavaScript Basics or Python Basics is needed.
AWS Lambda Basics
AWS Lambda runs your code in response to events and automatically manages the compute resources.
Lambda Execution Model
sequenceDiagram
Client->>API Gateway: HTTP Request
API Gateway->>Lambda: Invoke function
Lambda->>Lambda: Cold start (init)
Lambda->>Lambda: Execute handler
Lambda->>API Gateway: Response
API Gateway->>Client: HTTP Response
Lambda Function Structure
// Node.js Lambda handler
exports.handler = async (event, context) => {
// event: API Gateway event object
// context: Lambda runtime information
const { httpMethod, path, queryStringParameters, body, headers } = event;
return {
statusCode: 200,
headers: {
"Content-Type": "application/json",
"Access-Control-Allow-Origin": "https://dodatech.com"
},
body: JSON.stringify({
message: "Hello from DodaTech Lambda!",
path,
method: httpMethod
})
};
};
# Python Lambda handler
import json
def lambda_handler(event, context):
http_method = event.get("httpMethod")
path = event.get("path")
return {
"statusCode": 200,
"headers": {
"Content-Type": "application/json",
"Access-Control-Allow-Origin": "https://dodatech.com"
},
"body": json.dumps({
"message": "Hello from DodaTech Lambda!",
"path": path,
"method": http_method
})
}
Building a Serverless User API
Step 1: Project Structure
dodatech-serverless/
├── src/
│ ├── handlers/
│ │ ├── users.js
│ │ └── auth.js
│ ├── db.js
│ └── utils.js
├── template.yaml # SAM template
├── serverless.yml # Serverless Framework config
├── package.json
└── requirements.txt
Step 2: Lambda Handler
// src/handlers/users.js
const AWS = require("aws-sdk");
const dynamoDb = new AWS.DynamoDB.DocumentClient();
const { v4: uuidv4 } = require("uuid");
const USERS_TABLE = process.env.USERS_TABLE;
exports.listUsers = async (event) => {
try {
const params = {
TableName: USERS_TABLE,
Limit: 20
};
// Handle pagination
if (event.queryStringParameters?.cursor) {
params.ExclusiveStartKey = {
id: event.queryStringParameters.cursor
};
}
const result = await dynamoDb.scan(params).promise();
return formatResponse(200, {
success: true,
data: result.Items,
cursor: result.LastEvaluatedKey?.id || null
});
} catch (error) {
console.error("Error listing users:", error);
return formatResponse(500, {
success: false,
error: "Internal server error"
});
}
};
exports.createUser = async (event) => {
try {
const body = JSON.parse(event.body);
// Validate input
if (!body.name || !body.email) {
return formatResponse(422, {
success: false,
error: "Name and email are required"
});
}
const user = {
id: uuidv4(),
name: body.name,
email: body.email,
role: body.role || "member",
createdAt: new Date().toISOString()
};
await dynamoDb.put({
TableName: USERS_TABLE,
Item: user,
ConditionExpression: "attribute_not_exists(email)"
}).promise();
return formatResponse(201, {
success: true,
data: user
});
} catch (error) {
if (error.code === "ConditionalCheckFailedException") {
return formatResponse(409, {
success: false,
error: "User with this email already exists"
});
}
console.error("Error creating user:", error);
return formatResponse(500, {
success: false,
error: "Internal server error"
});
}
};
exports.getUser = async (event) => {
try {
const { id } = event.pathParameters;
const result = await dynamoDb.get({
TableName: USERS_TABLE,
Key: { id }
}).promise();
if (!result.Item) {
return formatResponse(404, {
success: false,
error: "User not found"
});
}
return formatResponse(200, {
success: true,
data: result.Item
});
} catch (error) {
console.error("Error getting user:", error);
return formatResponse(500, {
success: false,
error: "Internal server error"
});
}
};
exports.deleteUser = async (event) => {
try {
const { id } = event.pathParameters;
await dynamoDb.delete({
TableName: USERS_TABLE,
Key: { id },
ReturnValues: "ALL_OLD"
}).promise();
return formatResponse(204, null);
} catch (error) {
console.error("Error deleting user:", error);
return formatResponse(500, {
success: false,
error: "Internal server error"
});
}
};
function formatResponse(statusCode, body) {
return {
statusCode,
headers: {
"Content-Type": "application/json",
"Access-Control-Allow-Origin": "https://dodatech.com",
"Access-Control-Allow-Credentials": true
},
body: body ? JSON.stringify(body) : null
};
}
Step 3: Deploy with SAM (AWS Serverless Application Model)
# template.yaml
AWSTemplateFormatVersion: '2010-09-09'
Transform: AWS::Serverless-2016-10-31
Globals:
Function:
Runtime: nodejs20.x
Timeout: 10
MemorySize: 256
Environment:
Variables:
USERS_TABLE: !Ref UsersTable
Resources:
UsersTable:
Type: AWS::DynamoDB::Table
Properties:
TableName: dodatech-users
AttributeDefinitions:
- AttributeName: id
AttributeType: S
KeySchema:
- AttributeName: id
KeyType: HASH
BillingMode: PAY_PER_REQUEST
UsersApi:
Type: AWS::Serverless::Api
Properties:
StageName: prod
Cors:
AllowOrigin: "'https://dodatech.com'"
AllowMethods: "'GET,POST,PUT,DELETE,OPTIONS'"
AllowHeaders: "'Content-Type,Authorization'"
ListUsersFunction:
Type: AWS::Serverless::Function
Properties:
CodeUri: src/
Handler: handlers/users.listUsers
Events:
Api:
Type: Api
Properties:
RestApiId: !Ref UsersApi
Path: /users
Method: GET
Policies:
- DynamoDBReadPolicy:
TableName: !Ref UsersTable
CreateUserFunction:
Type: AWS::Serverless::Function
Properties:
CodeUri: src/
Handler: handlers/users.createUser
Events:
Api:
Type: Api
Properties:
RestApiId: !Ref UsersApi
Path: /users
Method: POST
Policies:
- DynamoDBCrudPolicy:
TableName: !Ref UsersTable
Step 4: Build and Deploy
# Build the SAM application
sam build
# Deploy to AWS
sam deploy --guided
# Expected output:
# CloudFormation outputs:
# UsersApi - https://api-id.execute-api.us-east-1.amazonaws.com/prod
# UsersTable - dodatech-users
Step 5: Test the API
# Create a user
curl -X POST https://api-id.execute-api.us-east-1.amazonaws.com/prod/users \
-H "Content-Type: application/json" \
-d '{"name":"Alice","email":"alice@example.com"}'
# Expected response:
{"success":true,"data":{"id":"uuid","name":"Alice","email":"alice@example.com","role":"member","createdAt":"2026-06-23T10:00:00Z"}}
# List users
curl https://api-id.execute-api.us-east-1.amazonaws.com/prod/users
# Expected response:
{"success":true,"data":[{"id":"uuid","name":"Alice","email":"alice"@example".com"}],"cursor":null}
Using the Serverless Framework
# serverless.yml
service: dodatech-serverless-api
provider:
name: aws
runtime: nodejs20.x
region: us-east-1
environment:
USERS_TABLE: ${self:service}-${sls:stage}-users
iam:
role:
statements:
- Effect: Allow
Action:
- dynamodb:Query
- dynamodb:Scan
- dynamodb:GetItem
- dynamodb:PutItem
- dynamodb:UpdateItem
- dynamodb:DeleteItem
Resource: !Sub arn:aws:dynamodb:*:*:table/${self:provider.environment.USERS_TABLE}
functions:
listUsers:
handler: src/handlers/users.listUsers
events:
- http:
path: users
method: GET
cors: true
createUser:
handler: src/handlers/users.createUser
events:
- http:
path: users
method: POST
cors: true
resources:
Resources:
UsersTable:
Type: AWS::DynamoDB::Table
Properties:
TableName: ${self:provider.environment.USERS_TABLE}
AttributeDefinitions:
- AttributeName: id
AttributeType: S
KeySchema:
- AttributeName: id
KeyType: HASH
BillingMode: PAY_PER_REQUEST
Environment Variables and Secrets
// Using environment variables
const TABLE_NAME = process.env.TABLE_NAME;
const STAGE = process.env.STAGE;
// Using AWS Secrets Manager
const AWS = require("aws-sdk");
const secretsManager = new AWS.SecretsManager();
async function getSecret(secretName) {
const result = await secretsManager.getSecretValue({
SecretId: secretName
}).promise();
return JSON.parse(result.SecretString);
}
Monitoring and Troubleshooting
// Structured logging
exports.handler = async (event) => {
const requestId = event.requestContext?.requestId;
console.log(JSON.stringify({
level: "info",
requestId,
path: event.path,
method: event.httpMethod,
timestamp: new Date().toISOString()
}));
try {
// Function logic
} catch (error) {
console.error(JSON.stringify({
level: "error",
requestId,
error: error.message,
Stack: error.Stack,
timestamp: new Date().toISOString()
}));
throw error;
}
};
CloudWatch Logs Queries
# Find errors in the last hour
fields @timestamp, @message
| filter @message like /error/i
| sort @timestamp desc
| limit 50
# Find slow invocations
fields @timestamp, @duration
| filter @duration > 3000
| sort @duration desc
| limit 20
Common Errors
Cold start latency — The first invocation after a period of inactivity takes 1-5 seconds longer. Keep functions warm with scheduled events or use Provisioned Concurrency for latency-sensitive endpoints.
Dependency packaging issues — Forgetting to include
node_modulesor Python packages in the deployment package. Use Lambda Layers for shared dependencies to keep deployment packages small.Timeout too short — Lambda default timeout is 3 seconds. Database operations and external API calls can exceed this. Set timeout appropriately (max 15 minutes for HTTP endpoints, 10-30 seconds for API Gateway).
DynamoDB hot partitions — Using sequential IDs as partition keys causes throttling. Use random UUIDs or composite keys that distribute writes evenly across partitions.
VPC without NAT Gateway — Lambda functions in a VPC cannot access the internet or public AWS services without a NAT Gateway. Use VPC endpoints for DynamoDB and S3, or deploy functions outside the VPC.
Missing IAM permissions — Lambda functions need explicit permissions for every AWS service they access. Use least-privilege IAM policies. Debug with CloudWatch logs to find missing permissions.
API Gateway timeout mismatch — API Gateway has a 29-second timeout. If your Lambda function takes longer, API Gateway returns 504 before the function completes. Keep Lambda execution under 29 seconds for synchronous invocations.
Practice Questions
- What is a Lambda cold start and how do you mitigate it?
- How do you handle CORS in a Serverless API?
- What is the difference between DynamoDB scan and query?
- How do you manage environment variables for different stages?
- What IAM permissions are needed for Lambda to write to DynamoDB?
Challenge
Build a complete Serverless API for a file metadata service. Use API Gateway for HTTP endpoints, Lambda for compute, DynamoDB for metadata storage, S3 for file storage, and SQS for async processing. Implement endpoints for upload URL generation (presigned S3 URL), file metadata CRUD, async metadata extraction via SQS and Lambda, search by filename and file type, and pagination with Cursor. Deploy using SAM or Serverless Framework.
FAQ
Built by the developers of DodaTech
Doda Browser, DodaZIP & Durga Antivirus Pro