En este capítulo monto el esqueleto que uso en todos mis proyectos: un Worker en TypeScript 7, con un enrutador, tipos generados para los bindings, secretos fuera del código y pruebas que se ejecutan en el mismo runtime que producción.
Para quién es y qué necesitas
Para quien no ha escrito nunca un Worker. Si ya tienes uno funcionando, salta a Bindings.
Antes de empezar:
- Node.js 22 o superior.
- Una cuenta de Cloudflare y la sesión iniciada con
npx wrangler login. - Saber qué es una API HTTP. No hace falta saber nada de Workers.
Al terminar tendrás un Worker en TypeScript 7 con un enrutador, tipos generados para los bindings, secretos fuera del código, pruebas que se ejecutan en el mismo runtime que producción y tu primer despliegue.
Qué es un Worker por dentro
Un Worker es un módulo de JavaScript que exporta handlers: funciones que Cloudflare llama cuando pasa algo. El más común es fetch, que recibe una petición HTTP y devuelve una respuesta.
flowchart LR
P[Petición HTTP] --> F[fetch]
C[Cron] --> S[scheduled]
Q[Mensaje de cola] --> QU[queue]
E[Correo entrante] --> EM[email]
F & S & QU & EM --> ENV[env: bindings<br/>DB, ATTACHMENTS, AI...]
Cada handler recibe env, un objeto con tus bindings (base de datos, bucket, cola, variables y secretos), y ctx, que permite, por ejemplo, seguir trabajando después de responder con ctx.waitUntil().
Wrangler o cf: qué CLI usar
Cloudflare tiene dos herramientas de línea de comandos:
| Wrangler | cf | |
|---|---|---|
| Estado | Estable, la de siempre | Beta (noviembre de 2026) |
| Configuración | wrangler.jsonc |
cloudflare.config.ts (con autocompletado) |
| Alcance | Workers y sus recursos | Toda la API de Cloudflare (DNS, zonas, seguridad…) |
En esta guía uso Wrangler, porque es estable y lo que encontrarás en casi toda la documentación. Si empiezas un proyecto nuevo y te apetece probar cf, la conversión es cf migrate, y los comandos equivalentes se buscan con cf cli search "<task>".
Crear el proyecto
npm create cloudflare@latest notes -- --type=hello-world --lang=tscd notesnpm install -D typescript@7 honoC3 (create cloudflare) crea esta estructura:
notes/├── src/index.ts ← your Worker├── test/ ← tests├── wrangler.jsonc ← configuration and bindings├── worker-configuration.d.ts ← generated types (do not edit)├── tsconfig.json└── package.jsonConfigurar TypeScript 7
Wrangler empaqueta tu código con esbuild, que quita los tipos sin comprobarlos. La comprobación la hace TypeScript por separado. Por eso el tsconfig.json es de “solo comprobar”:
{ "compilerOptions": { "target": "es2024", "lib": ["es2024"], "module": "preserve", "moduleResolution": "bundler", "strict": true, "noEmit": true, "isolatedModules": true, "skipLibCheck": true, "noUncheckedIndexedAccess": true }, "include": ["worker-configuration.d.ts", "src", "test"]}{ "scripts": { "dev": "wrangler dev", "types": "wrangler types", "check": "tsc", "test": "vitest run", "deploy": "npm run check && wrangler deploy" }}El primer Worker con un enrutador
Para una API con varias rutas uso Hono: es pequeño, está pensado para Workers y tipa los bindings.
import { Hono } from "hono";
const app = new Hono<{ Bindings: Env }>();
app.get("/api/health", (c) => c.json({ ok: true }));
app.get("/api/hello/:name", (c) => { const name = c.req.param("name"); return c.json({ message: `Hello, ${name}` });});
app.notFound((c) => c.json({ error: "Not found" }, 404));
app.onError((err, c) => { console.error(err); return c.json({ error: "Something went wrong" }, 500);});
export default app;Si no quieres dependencias, un Worker sin enrutador es así:
export default { async fetch(request, env, ctx): Promise<Response> { const url = new URL(request.url); if (url.pathname === "/api/health") { return Response.json({ ok: true }); } return new Response("Not found", { status: 404 }); },} satisfies ExportedHandler<Env>;satisfies ExportedHandler<Env> da tipos a request, env y ctx sin escribirlos.
Bindings: conectar el Worker con servicios
Un binding se declara en wrangler.jsonc y aparece en env. Así queda la configuración que iremos completando en los próximos capítulos:
{ "$schema": "node_modules/wrangler/config-schema.json", "name": "notes", "main": "src/index.ts", "compatibility_date": "2026-11-03", "observability": { "enabled": true }, "vars": { "ENVIRONMENT": "production", "PUBLIC_URL": "https://notes.mydomain.dev" }, "d1_databases": [ { "binding": "DB", "database_name": "notes", "database_id": "…" } ], "r2_buckets": [ { "binding": "ATTACHMENTS", "bucket_name": "notes-attachments" } ]}Después de tocar los bindings, regenera los tipos:
npx wrangler typesWrangler escribe worker-configuration.d.ts con la interfaz Env (y los tipos del runtime para tu compatibility_date):
interface Env { ENVIRONMENT: "production"; PUBLIC_URL: "https://notes.mydomain.dev"; DB: D1Database; ATTACHMENTS: R2Bucket;}Variables y secretos
Lo que más me ayudó a entenderlo fue dibujar dónde vive cada cosa:
flowchart LR
subgraph REPO[En el repositorio]
V[wrangler.jsonc<br/>vars: ENVIRONMENT, PUBLIC_URL]
end
subgraph FUERA[Fuera del repositorio]
DV[.dev.vars<br/>secretos en tu máquina]
SP[wrangler secret put<br/>secretos en producción]
end
V --> E[env en tu Worker]
DV -->|wrangler dev| E
SP -->|desplegado| E
| Qué | Dónde | Ejemplo | Visible en el panel |
|---|---|---|---|
| Configuración no sensible | vars en wrangler.jsonc |
URL pública, nombre del entorno | Sí |
| Secretos | wrangler secret put |
Claves de API, tokens de Stripe | No (solo el nombre) |
| Secretos en local | .dev.vars (en .gitignore) |
Los mismos, con valores de prueba | — |
| Secretos compartidos entre Workers | Secrets Store | Una clave usada por varios servicios | No |
npx wrangler secret put STRIPE_KEYSTRIPE_KEY=sk_test_xxxLos secretos aparecen en env igual que las variables. wrangler types añade a Env los nombres que encuentra en .dev.vars, así que basta con tenerlos ahí con un valor de prueba.
Desarrollar en local
npm run devwrangler dev levanta tu Worker en http://localhost:8787 con el mismo runtime de producción (workerd) y simula en local D1, KV, R2, Queues y Durable Objects. Los datos locales se guardan en .wrangler/state y no tocan producción.
| Necesidad | Cómo |
|---|---|
| Trabajar sin conexión con datos de prueba | Por defecto: todo local |
| Usar un recurso real (por ejemplo, Workers AI, que no tiene simulación) | "remote": true en ese binding |
| Probar con datos reales de una base de datos | Mejor no: copia los datos a local con wrangler d1 export |
Escribir pruebas en el runtime de Workers
Las pruebas se ejecutan con Vitest dentro de workerd, así que usan los mismos bindings simulados. El paquete es @cloudflare/vitest-plugin; antes se llamaba @cloudflare/vitest-pool-workers y se configuraba de otra forma, así que si ves defineWorkersConfig en un tutorial, es la versión antigua.
npm install -D vitest @cloudflare/vitest-pluginimport { cloudflareTest } from "@cloudflare/vitest-plugin";import { defineConfig } from "vitest/config";
export default defineConfig({ plugins: [ cloudflareTest({ wrangler: { configPath: "./wrangler.jsonc" }, // Tests never touch real resources, even if a binding has "remote": true. remoteBindings: false, }), ],});import { exports } from "cloudflare:workers";import { describe, expect, it } from "vitest";
describe("API", () => { it("responds on /api/health", async () => { const res = await exports.default.fetch("https://notes.test/api/health"); expect(res.status).toBe(200); expect(await res.json()).toEqual({ ok: true }); });});env y exports se importan de cloudflare:workers, igual que en el código del Worker, y exports.default.fetch() llama a tu Worker sin pasar por la red. SELF y env de cloudflare:test siguen funcionando, pero están obsoletos.
Desplegar
npm run deployEl primer despliegue te da una URL notes.<your-subdomain>.workers.dev. Cada despliegue crea una versión nueva; cómo gestionarlas está en Entornos y despliegues.
Los límites que hay que conocer
| Límite | Plan gratuito | Plan de pago |
|---|---|---|
| CPU por petición | 10 ms | 30 s por defecto, hasta 5 min |
| Memoria | 128 MB | 128 MB |
Subpeticiones (fetch, bindings) |
50 | 10.000 |
| Tamaño del Worker | 64 MiB | 64 MiB |
| Workers por cuenta | 100 | 500 |
| Duración de una petición HTTP | Sin límite mientras el cliente siga conectado | Igual |
Cuánto cuesta
Cuánto cuesta al mes
Supuestos: Una API con 2 millones de peticiones al mes y 2 ms de CPU de media
| Concepto | Importe |
|---|---|
| Plan de pago de WorkersPor cuenta. Incluye 10 M de peticiones y 30 M de ms de CPU | 5,00 $ |
| Peticiones2 M, dentro de lo incluido | 0,00 $ |
| CPU4 M de ms, dentro de lo incluido | 0,00 $ |
| Total al mes | 5,00 $ |
Precios consultados el 3 de noviembre de 2026. Pueden cambiar.
Con ese tráfico cabe también en el plan gratuito (son unas 67.000 peticiones al día). Pasarse a pago compensa por los límites de CPU, las métricas de 7 días y productos que solo existen en pago.
Resumen
- Un Worker exporta handlers; los servicios llegan por
envcomo bindings tipados conwrangler types. - TypeScript 7 comprueba; Wrangler empaqueta. Pon
tscantes de cada despliegue. - Variables en la configuración, secretos con
wrangler secret puty.dev.varsen local. - Siguiente capítulo: servir el frontend con Static Assets.
