Serverless Basics
10 examples for AWS Lambda with Node.js 24 - 7 basic and 3 intermediate - plus when to choose serverless vs containers.
Prerequisites
mkdir lambda-api && cd lambda-api
npm init -y
npm pkg set type=module
npm install @aws-sdk/client-dynamodb @aws-sdk/lib-dynamodb
npm install -D typescript@5.6 esbuild @types/aws-lambda @types/nodeFor handler patterns and cold starts, see Lambda Handler Patterns and Cold Start Mitigation.
Basic Examples
1. Minimal Async Handler
import type { APIGatewayProxyHandlerV2 } from "aws-lambda";
export const handler: APIGatewayProxyHandlerV2 = async (event) => {
return {
statusCode: 200,
headers: { "content-type": "application/json" },
body: JSON.stringify({ message: "hello", requestId: event.requestContext.requestId }),
};
};- Prefer
asynchandlers over callback style in new code - API Gateway HTTP API v2 events use
APIGatewayProxyHandlerV2 - Return structured objects; Lambda serializes the response
2. Parse JSON Body
import type { APIGatewayProxyHandlerV2 } from "aws-lambda";
export const handler: APIGatewayProxyHandlerV2 = async (event) => {
if (!event.body) {
return { statusCode: 400, body: JSON.stringify({ error: "missing body" }) };
}
const payload = JSON.parse(event.body) as { name?: string };
if (!payload.name) {
return { statusCode: 422, body: JSON.stringify({ error: "name required" }) };
}
return { statusCode: 201, body: JSON.stringify({ id: "1", name: payload.name }) };
};- Validate input with Zod at the boundary
- API Gateway may base64-encode bodies; check
isBase64Encoded - Keep handlers thin; call service functions for logic
3. Environment Configuration
import { z } from "zod";
const envSchema = z.object({
TABLE_NAME: z.string().min(1),
NODE_ENV: z.enum(["development", "production", "test"]).default("production"),
});
const env = envSchema.parse(process.env);
export function getTableName() {
return env.TABLE_NAME;
}- Parse env once at cold start, not per request
- Set variables in Lambda console or IaC (SAM, CDK, Terraform)
- Never commit secrets to the deployment zip
4. DynamoDB with SDK v3
import { DynamoDBClient } from "@aws-sdk/client-dynamodb";
import { DynamoDBDocumentClient, GetCommand } from "@aws-sdk/lib-dynamodb";
import type { APIGatewayProxyHandlerV2 } from "aws-lambda";
const client = DynamoDBDocumentClient.from(new DynamoDBClient({}));
export const handler: APIGatewayProxyHandlerV2 = async (event) => {
const id = event.pathParameters?.id;
const res = await client.send(
new GetCommand({ TableName: process.env.TABLE_NAME!, Key: { id } })
);
if (!res.Item) {
return { statusCode: 404, body: JSON.stringify({ error: "not found" }) };
}
return { statusCode: 200, body: JSON.stringify(res.Item) };
};- Create SDK clients outside the handler for connection reuse
- Use
@aws-sdk/lib-dynamodbfor plain JS objects - See @aws-sdk v3 for modular imports
5. SQS Event Handler
import type { SQSHandler } from "aws-lambda";
export const handler: SQSHandler = async (event) => {
for (const record of event.Records) {
const body = JSON.parse(record.body) as { orderId: string };
await processOrder(body.orderId);
}
};
async function processOrder(orderId: string) {
console.log(JSON.stringify({ event: "order_processed", orderId }));
}- SQS triggers batch records; partial batch failure reporting needs
ReportBatchItemFailures - Idempotency is required; SQS delivers at-least-once
- Long work may exceed Lambda timeout; consider Step Functions
6. Structured Logging
import type { APIGatewayProxyHandlerV2 } from "aws-lambda";
export const handler: APIGatewayProxyHandlerV2 = async (event) => {
const requestId = event.requestContext.requestId;
console.log(JSON.stringify({
level: "info",
requestId,
path: event.rawPath,
method: event.requestContext.http.method,
}));
return { statusCode: 200, body: JSON.stringify({ ok: true }) };
};- CloudWatch Logs ingests stdout JSON lines
- Include
requestIdfor correlation - Avoid
console.logstring concatenation with PII
7. Lambda vs Container Decision
| Signal | Prefer Lambda | Prefer containers (K8s/ECS) |
|---|---|---|
| Traffic shape | Spiky, intermittent | Steady 24/7 baseline |
| Request duration | Under 30s typical | Long-running streams |
| Connections | Stateless HTTP | WebSockets, gRPC long-lived |
| Ops model | No cluster to manage | Need sidecars, custom networking |
| Cold start sensitivity | Can tolerate 100-500ms | Sub-10ms p99 required |
- Hybrid is common: API on ECS, async workers on Lambda
- See Platform Deploy Basics for 12-factor containers
Intermediate Examples
8. esbuild Bundle for Lambda
// build.mjs
import * as esbuild from "esbuild";
await esbuild.build({
entryPoints: ["src/handler.ts"],
bundle: true,
platform: "node",
target: "node24",
outfile: "dist/handler.mjs",
format: "esm",
external: ["@aws-sdk/*"],
});{
"scripts": {
"build": "node build.mjs",
"zip": "cd dist && zip -r ../function.zip handler.mjs"
}
}- Bundle application code; keep
@aws-sdk/*external (provided by runtime or layer) - Smaller zips reduce cold start time
- See Cold Start Mitigation
9. SAM Template Snippet
AWSTemplateFormatVersion: "2010-09-09"
Transform: AWS::Serverless-2016-10-31
Resources:
ApiFunction:
Type: AWS::Serverless::Function
Properties:
Runtime: nodejs24.x
Handler: handler.handler
MemorySize: 512
Timeout: 10
Architectures: [arm64]
Environment:
Variables:
TABLE_NAME: !Ref OrdersTable
Events:
Api:
Type: HttpApi
Properties:
Path: /orders/{id}
Method: GETarm64(Graviton) often improves price/performance for Node- Set
Timeoutfrom p99 latency + downstream calls - Infrastructure as code is mandatory for reproducible deploys
10. Wrap Express with serverless-http
import express from "express";
import serverless from "serverless-http";
const app = express();
app.use(express.json());
app.get("/users", (_req, res) => res.json([]));
export const handler = serverless(app);- Fastest path to lift existing Express apps
- Cold starts suffer from full framework load
- Prefer
@fastify/aws-lambdaor dedicated handlers at scale - see serverless-express / aws-lambda-fastify
FAQs
Which Node runtime should we use?
nodejs24.x on Lambda for parity with Node 24 LTS locally. Align esbuild target with node24.
Lambda or Fargate for a REST API?
Steady traffic and low latency: Fargate or K8s. Spiky traffic and minimal ops: Lambda. Measure cost at your request volume.
Can Lambda run TypeScript directly?
No. Compile or bundle to JavaScript before deploy. Use esbuild in CI.
How do we handle database connections?
Use RDS Proxy or Data API for Aurora Serverless v2. Do not open a new PG pool per invocation without proxy.
Is VPC required?
Only to reach private RDS/ElastiCache. VPC adds ENI cold start latency; mitigate with Cold Start Mitigation.
Where do secrets live?
AWS Secrets Manager or SSM Parameter Store, loaded at init outside the handler hot path. See Secrets Managers.
Related
- Lambda Handler Patterns - event shapes
- Cold Start Mitigation - bundle and concurrency
- @aws-sdk v3 - modular AWS clients
- serverless-express / aws-lambda-fastify - framework adapters
- Serverless Best Practices - section checklist
Stack versions: This page was written for Node.js 24.18.0 (Active LTS), npm 10+, TypeScript 5.6+, Express 5, Fastify 5, and NestJS 11.