Cloudflare Workers con Claude Code: guía práctica de API edge
Crea una API Workers con Claude Code, Wrangler, KV/D1/R2, caché, rate limiting, logs y cabeceras seguras.
Cloudflare Workers ejecuta JavaScript y TypeScript en la red edge de Cloudflare. Edge significa que el código se ejecuta cerca del usuario, no solo en una región central. Es una buena opción para APIs pequeñas, webhooks, BFF, respuestas cacheadas, validaciones de seguridad y control de descargas desde R2.
Claude Code encaja bien porque un Worker se divide en piezas claras: un fetch handler, un wrangler.toml y bindings explícitos como KV, D1 y R2. Revisé la documentación oficial el 3 de junio de 2026; ten a mano Workers, Wrangler, bindings, KV, D1, R2, Cache API, Rate Limiting y Workers Logs. Como comparación interna, revisa serverless functions y AWS Lambda guide.
Qué vamos a construir
El ejemplo es una API de pedidos. Guarda datos en D1, lee configuración desde KV, escribe recibos en R2, cachea respuestas GET seguras, limita peticiones autenticadas, registra logs estructurados y añade cabeceras de seguridad.
flowchart LR
Client["Cliente"] --> Worker["Worker fetch handler"]
Worker --> D1["D1 pedidos"]
Worker --> KV["KV configuración"]
Worker --> R2["R2 recibos"]
Worker --> Cache["Cache API"]
Worker --> Logs["Workers Logs"]
Un binding es una capacidad externa inyectada en el Worker. Si declaras binding = "DB", el código la usa como env.DB.
Prompt inicial para Claude Code
Implementa una API de pedidos con Cloudflare Workers + TypeScript.
Archivos:
- src/index.ts
- wrangler.toml
- schema.sql
Requisitos:
- Usar module fetch handler
- GET /health devuelve JSON
- GET /orders/:id lee D1 y cachea solo salida pública segura por 30 segundos
- POST /orders valida JSON e inserta en D1
- Validar Authorization Bearer contra API_TOKEN
- Usar SETTINGS KV, DB D1, RECEIPTS R2 y API_RATE_LIMITER
- Añadir security headers a cada respuesta
- Registrar logs como objetos JSON
- Incluir comandos curl de verificación
Prohibido:
- Escribir secrets en wrangler.toml
- Asumir un servidor Node.js persistente
- Entregar pseudocódigo
Configuración con Wrangler
npm create cloudflare@latest claude-worker-api -- --type=hello-world
cd claude-worker-api
npm install -D typescript wrangler
npx wrangler --version
C3 puede generar wrangler.jsonc en proyectos nuevos. Wrangler admite JSON/JSONC y TOML; este artículo usa TOML por legibilidad. Si tu proyecto ya tiene wrangler.jsonc, conserva las mismas claves en JSONC.
name = "claude-worker-api"
main = "src/index.ts"
compatibility_date = "2026-06-03"
[vars]
PUBLIC_ENV = "production"
[observability]
enabled = true
head_sampling_rate = 1
[[d1_databases]]
binding = "DB"
database_name = "claude-worker-api"
database_id = "replace-with-d1-database-id"
[[kv_namespaces]]
binding = "SETTINGS"
id = "replace-with-kv-namespace-id"
[[r2_buckets]]
binding = "RECEIPTS"
bucket_name = "claude-worker-receipts"
[[ratelimits]]
name = "API_RATE_LIMITER"
namespace_id = "1001"
[ratelimits.simple]
limit = 60
period = 60
npx wrangler login
npx wrangler d1 create claude-worker-api
npx wrangler kv namespace create SETTINGS
npx wrangler r2 bucket create claude-worker-receipts
npx wrangler secret put API_TOKEN
Esquema D1
CREATE TABLE IF NOT EXISTS orders (
id TEXT PRIMARY KEY,
email TEXT NOT NULL,
amount INTEGER NOT NULL,
status TEXT NOT NULL DEFAULT 'pending',
created_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP
);
CREATE INDEX IF NOT EXISTS idx_orders_email ON orders(email);
npx wrangler d1 execute claude-worker-api --local --file=./schema.sql
npx wrangler d1 execute claude-worker-api --remote --file=./schema.sql
Worker ejecutable
export interface Env {
API_TOKEN: string;
PUBLIC_ENV: string;
DB: D1Database;
SETTINGS: KVNamespace;
RECEIPTS: R2Bucket;
API_RATE_LIMITER: RateLimit;
}
const securityHeaders = {
"content-security-policy": "default-src 'none'; frame-ancestors 'none'",
"x-content-type-options": "nosniff",
"x-frame-options": "DENY",
"referrer-policy": "no-referrer",
"permissions-policy": "camera=(), microphone=(), geolocation=()",
};
function json(data: unknown, init: ResponseInit = {}) {
return new Response(JSON.stringify(data), {
...init,
headers: {
"content-type": "application/json; charset=utf-8",
...securityHeaders,
...init.headers,
},
});
}
export default {
async fetch(request, env, ctx): Promise<Response> {
const url = new URL(request.url);
const requestId = crypto.randomUUID();
console.log({ event: "request_started", requestId, method: request.method, path: url.pathname });
if (url.pathname === "/health") {
const maintenance = await env.SETTINGS.get("maintenance");
return json({ ok: true, env: env.PUBLIC_ENV, maintenance: maintenance === "true" });
}
if (request.headers.get("authorization") !== `Bearer ${env.API_TOKEN}`) {
return json({ error: "unauthorized" }, { status: 401 });
}
const { success } = await env.API_RATE_LIMITER.limit({
key: request.headers.get("authorization")?.slice(-16) ?? "anonymous",
});
if (!success) return json({ error: "rate_limited" }, { status: 429 });
const match = url.pathname.match(/^\/orders\/([a-zA-Z0-9_-]+)$/);
if (request.method === "GET" && match) {
const cacheKey = new Request(url.toString(), { method: "GET" });
const cached = await caches.default.match(cacheKey);
if (cached) return cached;
const order = await env.DB.prepare(
"SELECT id, email, amount, status, created_at FROM orders WHERE id = ?"
).bind(match[1]).first();
if (!order) return json({ error: "not_found" }, { status: 404 });
const response = json({ order }, {
headers: { "cache-control": "public, max-age=30", "cache-tag": `order-${match[1]}` },
});
ctx.waitUntil(caches.default.put(cacheKey, response.clone()));
return response;
}
if (request.method === "POST" && url.pathname === "/orders") {
const body = await request.json<{ email?: string; amount?: number }>();
if (!body.email?.includes("@") || !Number.isInteger(body.amount) || body.amount <= 0) {
return json({ error: "invalid_order" }, { status: 400 });
}
const id = crypto.randomUUID();
await env.DB.prepare(
"INSERT INTO orders (id, email, amount, status) VALUES (?, ?, ?, ?)"
).bind(id, body.email, body.amount, "pending").run();
await env.RECEIPTS.put(`orders/${id}.json`, JSON.stringify({ id, email: body.email, amount: body.amount }), {
httpMetadata: { contentType: "application/json" },
});
console.log({ event: "order_created", requestId, orderId: id });
return json({ id, status: "pending" }, { status: 201 });
}
return json({ error: "not_found" }, { status: 404 });
},
} satisfies ExportedHandler<Env>;
Prueba local y despliegue
printf "API_TOKEN=dev-token\n" > .dev.vars
npx wrangler dev
curl http://localhost:8787/health
curl -X POST http://localhost:8787/orders \
-H "authorization: Bearer dev-token" \
-H "content-type: application/json" \
-d "{\"email\":\"masa@example.com\",\"amount\":1200}"
curl http://localhost:8787/orders/replace-with-created-id \
-H "authorization: Bearer dev-token"
npx wrangler secret put API_TOKEN
npx wrangler d1 execute claude-worker-api --remote --file=./schema.sql
npx wrangler deploy
npx wrangler tail
Casos de uso reales
Primero, formularios y pedidos. D1 guarda el estado, R2 guarda recibos o adjuntos y Workers Logs conserva evidencia operativa.
Segundo, recepción de webhooks. Verifica la firma, guarda el event ID en D1 y evita procesar duplicados.
Tercero, BFF: Backend for Frontend. El Worker oculta API keys, adapta la respuesta para la UI y cachea solo datos seguros.
Cuarto, entrega controlada de archivos desde R2. El objeto pesado vive en R2; autorización, logs y cabeceras viven en el Worker.
Errores comunes
No escribas Workers como si fueran servidores Node.js persistentes. Usa Web APIs, Request, Response, fetch, crypto y bindings.
No mezcles responsabilidades de almacenamiento. KV es para clave-valor pequeño, D1 para datos relacionales, R2 para objetos y Cache API para acelerar respuestas breves.
No caches respuestas privadas. Si dependen de cookie, token, email o usuario, evita caché compartida o diseña una clave y TTL muy estrictos.
No limites solo por IP. Usa API key, user ID o tenant ID cuando sea posible.
Qué plataforma elegir
Elige Workers para APIs HTTP de baja latencia cerca del caché y los bindings de Cloudflare. Elige Pages Functions cuando una web en Cloudflare Pages necesita poca lógica dinámica. Elige Cloud Run si necesitas contenedores, tareas largas o un framework completo. Elige Lambda si el flujo vive en AWS con S3, DynamoDB, EventBridge o SQS.
Prompt de revisión
Revisa esta implementación de Cloudflare Workers.
Comprueba:
- compatibilidad con Workers runtime
- coincidencia entre Env y bindings de wrangler
- fuga de secrets en código, logs o config
- uso de bind en D1 y riesgo de SQL injection
- uso inseguro de Cache API
- clave de Rate Limiting
- security headers en todas las respuestas
- pasos curl faltantes
Devuelve hallazgos por severidad, correcciones y pruebas.
CTA y nota de verificación
Migra primero un solo endpoint: /health, un GET de solo lectura o un webhook. Da a Claude Code archivos, bindings, comandos de verificación y prohibiciones. Para plantillas reutilizables, consulta products; para formar al equipo en revisión y operación, consulta training.
Resultado de probarlo en la práctica: Probé el flujo del artículo como cadena Wrangler, D1, Worker y curl; antes de producción, repite wrangler deploy, wrangler tail, cabeceras, caché y respuesta 429 en tu cuenta.
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.