Busca en todas las páginas de la documentación
Write Lambda handlers that are typed, idempotent, and init-optimized for Node.js 24 on AWS.
Quick-reference recipe card - copy-paste ready.
import type { APIGatewayProxyHandlerV2, Context } from "aws-lambda";
// Init phase (cold start) - runs once per execution environment
const tableName = process.env.TABLE_NAME!;
export const handler: APIGatewayProxyHandlerV2 = async (event, context) => {
context.callbackWaitsForEmptyEventLoop = false;
const id = event.pathParameters?.id;
if (!id) {
return { statusCode: 400, body: JSON.stringify({ error: "id required" }) };
}
const item = await getItem(tableName, id);
return {
statusCode: item ? 200 : 404,
body: JSON.stringify(item ?? { error: "not found" }),
};
};
async function getItem(table: string, id: string) {
return { id, table };
}When to reach for this: Every new Lambda function before adopting framework wrappers.
// src/handlers/orders.ts
import type {
APIGatewayProxyEventV2,
APIGatewayProxyResultV2,
Context,
SQSBatchResponse,
SQSEvent,
} from "aws-lambda";
import { processOrder } from "../services/orders.js";
// ---- HTTP API handler ----
export async function httpHandler(
event
What this demonstrates:
callbackWaitsForEmptyEventLoop = false ends invocation when handler returns (do not wait on open DB pool timers)APIGatewayProxyEventV2 for HTTP API| Style | Status | Notes |
|---|---|---|
async (event) => {} | Preferred | Return value becomes response |
async (event, context, callback) => {} | Legacy | Avoid in new TypeScript code |
| Callback-only sync | Deprecated | No promise support |
// INIT (outside handler) - cache clients, parse env
import { DynamoDBClient } from "@aws-sdk/client-dynamodb";
const ddb = new DynamoDBClient({});
export const handler = async () => {
// INVOKE (inside handler) - per-request logic only
await ddb.send(/* ... */);
};Lambda reuses the execution environment across invocations. Put expensive setup in module scope.
export const handler = async (_event: unknown, context: Context) => {
console.log(JSON.stringify({
awsRequestId: context.awsRequestId,
functionName: context.functionName,
remainingMs: context.getRemainingTimeInMillis(),
}));
getRemainingTimeInMillis() guards long loops before hard timeout.
| Source | Type import | Gotcha |
|---|---|---|
| HTTP API | APIGatewayProxyEventV2 | Base64 bodies |
| REST API | APIGatewayProxyEvent | v1 vs v2 types differ |
| SQS | SQSEvent | At-least-once delivery |
| S3 | S3Event | Event per object |
| EventBridge | EventBridgeEvent<string, T> | detail payload typing |
max, or RDS Proxy.callbackWaitsForEmptyEventLoop = false - hangs until pool idle. Fix: set false or await pool.end() (usually wrong for reuse).event logging - PII in CloudWatch. Fix: log ids and metadata only.| Alternative | Use When | Don't Use When |
|---|---|---|
| Raw typed handlers | Full control, smallest bundle | Team wants Express routing |
| serverless-http | Lift Express app quickly | Cold start sensitive APIs |
| @fastify/aws-lambda | Fastify with Lambda bridge | Already on Express |
| Lambda Function URLs | Simple public HTTP without API GW | Need WAF, throttling, API keys |
Return statusCode: 500 for HTTP APIs. Throw only when you want Lambda to retry (async invocations) or mark the batch item failed.
EventBridgeEvent<"OrderCreated", { orderId: string }> and narrow on event["detail-type"].
One exported handler per Lambda function in AWS. Share code via services/ imports; do not multiplex unrelated triggers in one handler unless using a router library.
@nestjs/platform-aws-lambda wraps bootstrap. Still set callbackWaitsForEmptyEventLoop in the adapter bootstrap.
Import handler and pass fixture events from src/__fixtures__/apigw-v2-get.json. No need to start SAM for unit tests.
@middy/core wraps handlers for JSON parsing, CORS, and error normalization. Useful for raw handlers; less needed with Express/Fastify adapters.
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.