Serverless Functions con Claude Code: Lambda y Workers
Guia practica para crear serverless functions con Claude Code: prompt, plataforma, env/secrets, idempotencia, retries, tests y deploy.
Las serverless functions son piezas pequenas de codigo que se ejecutan por cada evento o solicitud HTTP, sin mantener un servidor siempre encendido. Funcionan muy bien para webhooks, APIs pequenas, entradas de procesamiento de imagenes o CSV, y reglas edge. Pero siguen exigiendo decisiones serias sobre timeout, retry, secrets, permisos, logs y coste.
Claude Code ayuda porque puede mantener en el mismo contexto el handler, el fixture del evento, los tests, las notas de despliegue y la checklist de revision. El flujo seguro no es dejar que despliegue solo. El flujo seguro es: escribir requisitos, elegir runtime y plataforma, reproducir el evento en local, separar configuracion y secrets, disenar idempotencia, probar fallos y pedir revision humana para exposicion y coste.
Usa la documentacion oficial como referencia: AWS Lambda Documentation, Lambda con Node.js, Cloudflare Workers development and testing y Workers get started guide. Para profundizar, revisa la guia AWS Lambda, la guia Cloudflare Workers, la guia de desarrollo de APIs y la guia de gestion de secrets.
Primero El Caso De Uso
Serverless funciona mejor cuando el trabajo es corto, esta ligado a eventos y puede repetirse sin romper datos.
| Caso de uso | Por que encaja | Claude Code puede redactar | Revision humana |
|---|---|---|---|
| Webhook de pagos o formularios | Una request se convierte en un evento | Verificacion de firma, fixture, respuestas de error | Secrets, duplicados, replay |
| Entrada de resize o CSV | El trabajo pesado va a storage o queue | Validacion, job ID, logs JSON | Tamano de archivo, timeout, cleanup |
| API JSON interna | No hace falta servidor persistente | Handler, tests, route | Auth, CORS, exposicion, rate limit |
| Redirect/cache edge | Respuesta cerca del usuario | Worker route, headers cache, rollout | Purga cache, datos personales, SEO |
flowchart LR
A[Prompt de requisitos] --> B[Elegir Lambda o Workers]
B --> C[Reproducir evento local]
C --> D[Separar env y secrets]
D --> E[Idempotencia y retry]
E --> F[Tests]
F --> G[Deploy a dev]
G --> H[Logs y cleanup]
Prompt Para Claude Code
Crea una funcion serverless minima en Node.js.
Objetivo:
- Manejar POST /orders y devolver una respuesta accepted
- Ejecutarse localmente con node local-test.mjs
- Asumir eventos AWS Lambda HTTP API v2
Requisitos:
- Explicar index.mjs, events/create-order.json, local-test.mjs e index.test.mjs
- Devolver 400 si falta idempotency-key
- Devolver la misma respuesta si se repite idempotency-key
- Separar invalid JSON, invalid input y unsupported route
- Logs en JSON sin secrets ni datos personales
- Incluir checklist antes de deploy
Restricciones:
- Sin paquetes npm externos
- En produccion, la idempotencia debe usar DynamoDB, KV u otro storage durable
- IAM, URLs publicas y recursos facturables requieren confirmacion humana
Lambda O Workers
AWS Lambda encaja cuando necesitas eventos AWS, IAM, S3, DynamoDB, SQS o EventBridge. Cloudflare Workers encaja cuando el trabajo principal es HTTP en el edge: redirects, APIs ligeras, cache, validacion simple, KV/D1/R2. Vercel Functions es util dentro de Next.js, pero aqui usamos Lambda y Workers para mantener conceptos verificables.
| Criterio | AWS Lambda | Cloudflare Workers |
|---|---|---|
| Mejor para | Integraciones AWS, APIs de negocio, jobs async | HTTP edge, routing, cache, APIs ligeras |
| Local | Node.js, SAM, AWS CLI | Wrangler |
| Permisos | IAM role y policy | Bindings, secrets, permisos de cuenta |
| Riesgo comun | IAM amplio, coste VPC/NAT, volumen de logs | Drift de bindings, limites runtime, KV consistency |
Handler Lambda Ejecutable
// index.mjs
import crypto from "node:crypto";
const localIdempotencyStore = new Map();
function json(statusCode, body) {
return {
statusCode,
headers: { "content-type": "application/json" },
body: JSON.stringify(body),
};
}
function readHeader(headers = {}, name) {
const target = name.toLowerCase();
const found = Object.entries(headers).find(([key]) => key.toLowerCase() === target);
return found?.[1];
}
function parseBody(event) {
if (!event.body) return {};
const raw = event.isBase64Encoded
? Buffer.from(event.body, "base64").toString("utf8")
: event.body;
return JSON.parse(raw);
}
export async function handler(event = {}, context = {}) {
const method = event.requestContext?.http?.method ?? event.httpMethod ?? "GET";
const path = event.rawPath ?? event.path ?? "/";
const requestId = context.awsRequestId ?? crypto.randomUUID();
console.log(JSON.stringify({ level: "info", message: "request.start", requestId, method, path }));
if (method !== "POST" || path !== "/orders") {
return json(404, { error: "not_found" });
}
const idempotencyKey = readHeader(event.headers, "idempotency-key");
if (!idempotencyKey) {
return json(400, { error: "idempotency_key_required" });
}
if (localIdempotencyStore.has(idempotencyKey)) {
return json(200, { ...localIdempotencyStore.get(idempotencyKey), replay: true });
}
let body;
try {
body = parseBody(event);
} catch {
return json(400, { error: "invalid_json" });
}
if (!Number.isFinite(body.amount) || body.amount <= 0 || typeof body.currency !== "string") {
return json(400, { error: "invalid_order" });
}
const accepted = {
orderId: crypto.randomUUID(),
status: "accepted",
amount: body.amount,
currency: body.currency,
};
localIdempotencyStore.set(idempotencyKey, accepted);
console.log(JSON.stringify({ level: "info", message: "order.accepted", requestId, orderId: accepted.orderId }));
return json(202, accepted);
}
{
"version": "2.0",
"routeKey": "POST /orders",
"rawPath": "/orders",
"headers": {
"content-type": "application/json",
"idempotency-key": "demo-key-001"
},
"requestContext": {
"http": {
"method": "POST",
"path": "/orders"
}
},
"body": "{\"amount\":3200,\"currency\":\"EUR\"}",
"isBase64Encoded": false
}
// local-test.mjs
import { readFile } from "node:fs/promises";
import { handler } from "./index.mjs";
const eventPath = process.argv[2] ?? "events/create-order.json";
const event = JSON.parse(await readFile(eventPath, "utf8"));
const first = await handler(event, { awsRequestId: "local-001" });
const second = await handler(event, { awsRequestId: "local-002" });
console.log("first:", first.statusCode, first.body);
console.log("second:", second.statusCode, second.body);
node local-test.mjs events/create-order.json
La Map solo sirve para demo local. En produccion, usa escrituras condicionales en DynamoDB, una restriccion unica de base de datos, Cloudflare KV/D1 u otro almacenamiento durable.
Tests
// index.test.mjs
import crypto from "node:crypto";
import test from "node:test";
import assert from "node:assert/strict";
import { handler } from "./index.mjs";
function event(overrides = {}) {
return {
rawPath: "/orders",
headers: { "idempotency-key": crypto.randomUUID() },
requestContext: { http: { method: "POST" } },
body: JSON.stringify({ amount: 1200, currency: "EUR" }),
isBase64Encoded: false,
...overrides,
};
}
test("requires idempotency-key", async () => {
const result = await handler(event({ headers: {} }), {});
assert.equal(result.statusCode, 400);
});
test("accepts a valid order", async () => {
const result = await handler(event(), {});
assert.equal(result.statusCode, 202);
assert.equal(JSON.parse(result.body).status, "accepted");
});
test("rejects invalid JSON", async () => {
const result = await handler(event({ body: "not-json" }), {});
assert.equal(result.statusCode, 400);
});
node --test index.test.mjs
Version Workers Con KV
// src/worker.js
export default {
async fetch(request, env) {
const url = new URL(request.url);
if (request.method !== "POST" || url.pathname !== "/orders") {
return Response.json({ error: "not_found" }, { status: 404 });
}
if (request.headers.get("x-webhook-secret") !== env.WEBHOOK_SECRET) {
return Response.json({ error: "unauthorized" }, { status: 401 });
}
const idempotencyKey = request.headers.get("idempotency-key");
if (!idempotencyKey) {
return Response.json({ error: "idempotency_key_required" }, { status: 400 });
}
const existing = await env.IDEMPOTENCY_KV.get(idempotencyKey, "json");
if (existing) {
return Response.json({ ...existing, replay: true });
}
const body = await request.json();
if (!Number.isFinite(body.amount) || typeof body.currency !== "string") {
return Response.json({ error: "invalid_order" }, { status: 400 });
}
const accepted = {
orderId: crypto.randomUUID(),
status: "accepted",
amount: body.amount,
currency: body.currency,
};
await env.IDEMPOTENCY_KV.put(idempotencyKey, JSON.stringify(accepted), {
expirationTtl: 86400,
});
return Response.json(accepted, { status: 202 });
},
};
npm create cloudflare@latest serverless-orders-worker
cd serverless-orders-worker
npx wrangler kv namespace create IDEMPOTENCY_KV
npx wrangler secret put WEBHOOK_SECRET
npx wrangler dev
Errores Frecuentes Y Checklist
El primer error es asumir ejecucion exactamente una vez. Webhooks, colas, eventos async y navegadores pueden reintentar. El segundo es filtrar secrets en logs o ejemplos. El tercero es aceptar permisos amplios como Resource: "*". El cuarto es publicar una URL sin owner, auth, CORS, rate limit, retencion de logs y plan de borrado.
| Check | Que confirmar |
|---|---|
| Requisitos | Input, output, owner y errores documentados |
| Runtime | Lambda Node.js runtime o Workers compatibility_date explicito |
| Local | Fixture y node --test pasan |
| Env/secrets | Configuracion y secrets separados |
| Idempotencia | Retry no duplica cobros ni registros |
| Timeout/retry | Trabajo lento va a queue o job durable |
| Observabilidad | Logs JSON, error rate, alertas y retention definidos |
| Cleanup | Comandos de borrado o pasos de dashboard escritos |
zip function.zip index.mjs
aws lambda update-function-code \
--function-name serverless-orders-dev \
--zip-file fileb://function.zip
npx wrangler deploy
Prompt final:
Revisa esta serverless function antes de publicar.
Separa blocking issues, non-blocking improvements y human confirmations.
Comprueba idempotencia, timeout/retry, secrets, IAM o bindings, logs,
reproducibilidad local, cleanup, enlaces oficiales y enlaces internos.
ClaudeCodeLab empaqueta estos patrones en productos y templates de Claude Code. Para disenar permisos AWS, CLAUDE.md, prompts de revision y aprobaciones de deploy en un repositorio real, revisa la pagina de consultoria y formacion Claude Code.
En la prueba practica, el mayor beneficio fue crear primero el fixture de evento. Claude Code es rapido, pero solo maneja bien retries, secrets y cleanup cuando esas restricciones estan en el primer prompt.
PDF gratis: cheatsheet de Claude Code
Introduce tu email y descarga una hoja con comandos, hábitos de revisión y flujos seguros.
Cuidamos tus datos y no enviamos spam.
Sobre el autor
Masa
Ingeniero enfocado en workflows prácticos con Claude Code.
Artículos relacionados
Checklist de permisos antes de que Claude Code edite un sitio de cliente
Guía para agencias que quieren usar IA en landing pages sin tocar zonas sensibles.
Convierte tickets de soporte SaaS en pasos reproducibles con Claude Code
Flujo para transformar reportes vagos en pasos, evidencia y una nota útil para ingeniería.
Convierte tus notas viejas de Obsidian en instrucciones para Claude Code en 10 minutos
Rutina de 10 minutos para separar tus notas de Obsidian en hechos, decisiones y dudas, y darle a Claude Code instrucciones que sí funcionan.