La estructura moderna: export default y sus tres argumentos
La forma moderna de escribir un Worker es un módulo de ES que exporta un objeto con un método fetch. Dentro recibe tres argumentos —request, env y ctx— y cada uno abre una dimensión distinta de la plataforma.
La forma moderna de escribir un Worker es un módulo de ES que exporta un objeto por defecto con un método fetch. Dentro, ese método recibe tres argumentos —request, env y ctx— y cada uno abre una dimensión distinta de la plataforma: la petición, tus recursos y el ciclo de vida. Dominar esta firma es dominar la puerta de entrada a todo lo demás.
- La sintaxis
export defaultcon un métodofetchy por qué sustituyó al formato antiguo. - Qué es
requesty qué garantías te da. - Qué es
envy por qué los bindings viven ahí y no en variables globales. - Qué es
ctxy para qué sirvenwaitUntilypassThroughOnException.
El módulo por defecto
Un Worker moderno es un módulo de ES. Exporta, con export default, un objeto cuyas claves son los handlers que la plataforma sabe invocar. Para HTTP, la clave es fetch:
export default {
async fetch(request, env, ctx) {
return new Response("Hola");
},
};
Esto se llama el formato de módulos (ES modules syntax), y desde 2023 es el estándar. El objeto exportado puede tener otros handlers además de fetch —cada uno responde a un tipo de evento distinto—, y todos comparten la misma idea: exportas funciones con nombre y el runtime llama a la que corresponde. Un mismo Worker puede atender HTTP, tareas programadas y mensajes de una cola a la vez:
export default {
async fetch(request, env, ctx) {
return new Response("via HTTP");
},
async scheduled(event, env, ctx) {
// se ejecuta segun un cron, sin peticion HTTP
},
};
Ese patrón —un objeto con handlers con nombre— no es solo estético: le da al runtime una forma uniforme de despachar cualquier evento a la función correcta, y a ti un único lugar donde declarar todo lo que tu Worker sabe hacer.
Antes se escribía registrando un listener sobre el evento fetch, al estilo heredado de los Service Workers del navegador, respondiendo con event.respondWith(...). Todavía funciona, pero es el formato legacy: no recibe env como argumento (los bindings aparecían como variables globales) y no encaja con las capacidades modernas. Si ves ese estilo, sabes que es código antiguo. En este track usamos siempre export default.
Los tres argumentos
Cada vez que llega una petición, el runtime invoca tu fetch con tres argumentos, siempre en el mismo orden:
export default {
async fetch(request: Request, env: Env, ctx: ExecutionContext): Promise<Response> {
// request: la peticion entrante
// env: tus bindings y variables
// ctx: el control del ciclo de vida
return new Response("ok");
},
};
request
La Request entrante, del estándar web. Todo lo que el cliente pidió: método, URL, cabeceras y cuerpo.
env
El objeto con tus bindings: bases de datos D1, namespaces KV, buckets R2, secretos y variables. Tu puerta a los recursos.
ctx
El ExecutionContext: controla el ciclo de vida de la invocación con waitUntil y passThroughOnException.
request y env
El primer argumento, request, ya lo conoces del fetch handler: es la petición del estándar web, inmutable, con el método, la URL, las cabeceras y el cuerpo de lo que pidió el cliente.
El segundo, env, es la novedad clave del formato de módulos. Es un objeto que contiene todos tus bindings: si declaras una base de datos D1, la encontrarás en env.MI_DB; si declaras un namespace KV, en env.MI_KV; tus secretos y variables de entorno también viven ahí.
export default {
async fetch(request, env, ctx) {
const valor = await env.MI_KV.get("clave");
return new Response(valor ?? "vacio");
},
};
Que env sea un argumento —y no un puñado de variables globales, como en el formato antiguo— no es un detalle estético. Significa que tus recursos se te pasan en cada invocación, de forma explícita y aislada. Eso es lo que hace posible el modelo de capacidades de la plataforma: tu código no busca credenciales en el entorno global, recibe referencias a los recursos que tiene permiso para usar.
En TypeScript, das forma a env con una interfaz Env que describe qué bindings existen. Así el editor te autocompleta y el compilador te avisa si tocas algo que no declaraste:
interface Env {
MI_KV: KVNamespace;
MI_DB: D1Database;
API_TOKEN: string;
}
¿De dónde salen esos bindings? Los declaras en wrangler.jsonc: cada namespace de KV, cada base de datos D1 o cada variable que ates a tu Worker aparece luego como una propiedad de env con el nombre que le diste. La configuración y el tipo Env son las dos caras de lo mismo: una dice qué recursos existen, el otro se lo cuenta al compilador.
Algunos de los tipos de binding que declararás con el tiempo:
KVNamespace— almacén clave-valor global de lectura rápida.D1Database— base de datos SQL relacional.R2Bucket— almacenamiento de objetos y archivos.DurableObjectNamespace— estado coordinado con identidad propia.
Cada uno aparece en env con el nombre que le des; el tipo Env solo pone nombre y forma a lo que ya existe en la configuración.
Las variables no sensibles pueden vivir en wrangler.jsonc, pero los secretos —tokens, claves de API— se cargan aparte, con wrangler secret put, y nunca se escriben en el repositorio. Aun así, tu código los lee igual: como una propiedad más de env. La plataforma se encarga de que estén disponibles en tiempo de ejecución sin exponerlos en el fuente.
ctx: el ciclo de vida
El tercer argumento, ctx, un ExecutionContext, te da control sobre el ciclo de vida de esta invocación concreta. Tiene dos métodos que importan:
ctx.waitUntil(promesa)— extiende la vida del Worker hasta que esa promesa termine, después de haber devuelto la respuesta. Sirve para trabajo que no debe retrasar al usuario: registrar analíticas, escribir un log, refrescar una caché. Devuelves laResponseya, y el trabajo secundario sigue en segundo plano.ctx.passThroughOnException()— le dice al runtime que, si tu código lanza una excepción no capturada, deje pasar la petición al origen en lugar de fallar. Útil cuando tu Worker está delante de otro servidor.
export default {
async fetch(request, env, ctx) {
const respuesta = new Response("visto");
ctx.waitUntil(env.LOGS.put(crypto.randomUUID(), request.url));
return respuesta;
},
};
Sin ctx.waitUntil, cualquier trabajo pendiente al devolver la respuesta se cancelaría: el runtime da por terminada la invocación en cuanto respondes. waitUntil es la forma correcta de decir “he terminado con el usuario, pero aún tengo tarea de fondo que debe completarse”.
Los tres argumentos no son casualidad: son los tres ejes sobre los que gira todo Worker. request es el mundo exterior —lo que el cliente te pide, sobre lo que no tienes control—. env es tu mundo interior —los recursos que la plataforma pone a tu disposición, declarados y aislados, tu única vía legítima hacia el estado y hacia otros servicios—. Y ctx es el tiempo —el control sobre cuándo termina de verdad tu invocación, que te permite desacoplar lo que el usuario necesita ya de lo que puede ocurrir después—. Cuando entiendas que casi cualquier cosa que quieras hacer cae en uno de estos tres cubos —leer la petición, tocar un recurso, o gestionar el ciclo de vida—, la firma con request, env y ctx deja de ser una convención que memorizas y pasa a ser el índice mental de la plataforma. Cada nivel posterior de este track no hace más que profundizar en uno de estos tres argumentos.
- Escribe de memoria un
export defaultcon unasync fetch(request, env, ctx)y anota los tipos de cada argumento. - Declara una interfaz
Envcon un binding de KV y un secreto, y tipa el argumentoenvcon ella. - Explica por qué
envcomo argumento es más seguro que buscar credenciales en variables globales. - Inventa un caso de uso para
ctx.waitUntil: algo que quieras hacer después de responder al usuario sin hacerle esperar.