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
Workflow de Obsidian a CLAUDE.md con Claude Code
Convierte notas de trabajo de Obsidian en notas operativas de CLAUDE.md para no repetir contexto.
Claude Code Revenue CTA Routing: de artículos a PDF, Gumroad y consulta
Un flujo con Claude Code para dirigir lectores a PDF gratis, Gumroad o consulta según intención.
Reglas de handoff para equipos con Claude Code: evidencia, permisos, rollback e ingresos
Formato práctico para entregar trabajo de Claude Code con pruebas, permisos, rollback, PDF gratis, Gumroad y consulta.