Patrones de Handler de Lambda
Escribe handlers de Lambda que sean tipados, idempotentes y optimizados para la inicialización para Node.js 24 en AWS.
Receta
Tarjeta de receta de referencia rápida - lista para copiar y pegar.
import type { APIGatewayProxyHandlerV2, Context } from "aws-lambda";
// Fase de inicialización (arranque en frío) - se ejecuta una vez por entorno de ejecución
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 };
}Cuándo usarlo: Para cada nueva función Lambda antes de adoptar envoltorios de framework.
Ejemplo de trabajo
// src/handlers/orders.ts
import type {
APIGatewayProxyEventV2,
APIGatewayProxyResultV2,
Context,
SQSBatchResponse,
SQSEvent,
} from "aws-lambda";
import { processOrder } from "../services/orders.js";
// ---- Handler de API HTTP ----
export async function httpHandler(
event: APIGatewayProxyEventV2,
context: Context
): Promise<APIGatewayProxyResultV2> {
context.callbackWaitsForEmptyEventLoop = false;
if (event.requestContext.http.method === "POST" && event.rawPath === "/orders") {
const body = event.body ? JSON.parse(event.body) : {};
const order = await processOrder(body);
return { statusCode: 201, body: JSON.stringify(order) };
}
return { statusCode: 404, body: JSON.stringify({ error: "not found" }) };
}
// ---- Lote SQS con fallos parciales ----
export async function sqsHandler(event: SQSEvent): Promise<SQSBatchResponse> {
const batchItemFailures: { itemIdentifier: string }[] = [];
await Promise.all(
event.Records.map(async (record) => {
try {
const msg = JSON.parse(record.body) as { orderId: string };
await processOrder(msg);
} catch {
batchItemFailures.push({ itemIdentifier: record.messageId });
}
})
);
return { batchItemFailures };
}Lo que esto demuestra:
callbackWaitsForEmptyEventLoop = falsefinaliza la invocación cuando el handler retorna (no espera a los temporizadores de pool de DB abiertos)APIGatewayProxyEventV2tipado para API HTTP- Respuesta de fallo parcial de lote SQS para reintentos sin reprocesar los éxitos
Análisis profundo
Estilos de Handler
| Estilo | Estado | Notas |
|---|---|---|
async (event) => {} | Preferido | El valor de retorno se convierte en respuesta |
async (event, context, callback) => {} | Heredado | Evitar en código TypeScript nuevo |
| Solo callback síncrono | Obsoleto | Sin soporte para promesas |
Fase de Inicialización vs Invocación
// INIT (fuera del handler) - clientes de caché, analizar env
import { DynamoDBClient } from "@aws-sdk/client-dynamodb";
const ddb = new DynamoDBClient({});
export const handler = async () => {
// INVOKE (dentro del handler) - solo lógica por solicitud
await ddb.send(/* ... */);
};Lambda reutiliza el entorno de ejecución entre invocaciones. Coloca la configuración costosa en el ámbito del módulo.
Campos de Contexto que Realmente Usas
export const handler = async (_event: unknown, context: Context) => {
console.log(JSON.stringify({
awsRequestId: context.awsRequestId,
functionName: context.functionName,
remainingMs: context.getRemainingTimeInMillis(),
}));
};getRemainingTimeInMillis() protege los bucles largos antes del tiempo de espera forzado.
Resumen de la Fuente de Eventos
| Fuente | Importación de tipo | Trampa |
|---|---|---|
| API HTTP | APIGatewayProxyEventV2 | Cuerpos Base64 |
| API REST | APIGatewayProxyEvent | Los tipos v1 vs v2 difieren |
| SQS | SQSEvent | Entrega al menos una vez |
| S3 | S3Event | Evento por objeto |
| EventBridge | EventBridgeEvent<string, T> | Tipado de carga detail |
Errores comunes
- Nuevo pool de DB por invocación dentro del handler - agota las conexiones. Solución: pool con ámbito de módulo con
maxbajo, o RDS Proxy. - Olvidar
callbackWaitsForEmptyEventLoop = false- se cuelga hasta que el pool esté inactivo. Solución: establecer en falso oawait pool.end()(generalmente incorrecto para la reutilización). - Handlers SQS no idempotentes - cargos duplicados. Solución: claves de idempotencia en DynamoDB.
- Lanzar errores en errores de validación 4xx - Lambda marca la invocación como fallida; puede reintentar. Solución: devolver respuesta 4xx, no lanzar.
- Registro de
eventgrande - PII en CloudWatch. Solución: registrar solo IDs y metadatos. - Mezclar tipos de API Gateway v1 y v2 - la compilación pasa, falta el campo en tiempo de ejecución. Solución: hacer coincidir el tipo de integración en IaC.
Alternativas
| Alternativa | Cuándo usar | Cuándo NO usar |
|---|---|---|
| Handlers tipados puros | Control total, paquete más pequeño | El equipo quiere enrutamiento Express |
| serverless-http | Levantar la aplicación Express rápidamente | APIs sensibles al arranque en frío |
| @fastify/aws-lambda | Fastify con puente Lambda | Ya estás en Express |
| URLs de función Lambda | HTTP público simple sin API GW | Necesitas WAF, limitación, claves de API |
Preguntas frecuentes
¿Los handlers deben retornar o lanzar en caso de 500?
Retorna statusCode: 500 para APIs HTTP. Lanza solo cuando quieras que Lambda reintente (invocaciones asíncronas) o marque el elemento del lote como fallido.
¿Cómo tipifico los detalles de eventos personalizados?
EventBridgeEvent<"OrderCreated", { orderId: string }> y refina en event["detail-type"].
¿Un archivo de handler o muchos?
Un handler exportado por función Lambda en AWS. Comparte código a través de importaciones de services/; no multiplexes disparadores no relacionados en un solo handler a menos que uses una biblioteca de enrutamiento.
¿NestJS cambia el patrón?
@nestjs/platform-aws-lambda envuelve el bootstrap. Aún así, establece callbackWaitsForEmptyEventLoop en el bootstrap del adaptador.
¿Cómo pruebo los handlers localmente?
Importa el handler y pasa eventos de prueba desde src/__fixtures__/apigw-v2-get.json. No es necesario iniciar SAM para pruebas unitarias.
¿Qué hay del middleware Middy?
@middy/core envuelve los handlers para el análisis JSON, CORS y la normalización de errores. Útil para handlers puros; menos necesario con adaptadores Express/Fastify.
Relacionado
- Conceptos básicos de Serverless - Lambda vs contenedores
- Mitigación de arranque en frío - optimización de inicialización
- @aws-sdk v3 - clientes en fase de inicialización
- serverless-express / aws-lambda-fastify - envoltorios de framework
- Mejores prácticas de Serverless - lista de verificación de la sección
Versiones de pila: Esta página fue escrita para Node.js 24.18.0 (LTS activa), npm 10+, TypeScript 5.6+, Express 5, Fastify 5 y NestJS 11.