wandres.dev
BINDINGS · el modelo de acceso

El objeto env

El segundo argumento de fetch es la puerta a todas las capacidades del Worker: env es un mapa que la plataforma puebla con un objeto vivo por cada binding declarado. Cómo se lee env.MI_KV o env.DB, en qué se diferencia del process.env de Node y cómo alcanzar env fuera del handler con el import de cloudflare:workers.

⏱ 14 min

Si un binding es una capacidad, env es el llavero donde todas ellas cuelgan. Cada handler de tu Worker lo recibe como segundo argumento, y en sus propiedades viven, ya resueltos, los objetos que la plataforma conectó a partir de tus bindings. Aprender a leer env es aprender a pensar en el entorno de un Worker no como un saco de variables de texto, sino como un conjunto de clientes vivos que puedes usar de inmediato.

🎯 Al terminar esta lección sabrás
  • Ubicar env como el segundo argumento de los handlers y explicar de dónde sale.
  • Leer bindings desde env con notación de punto: env.MI_KV, env.DB, env.AI.
  • Distinguir env del process.env de Node y de cualquier variable global.
  • Alcanzar env fuera del handler con el import de cloudflare:workers y con this.env.

El segundo argumento del handler

Un Worker en formato de módulos ES exporta un objeto con handlers. El más común es fetch, y su firma tiene tres argumentos que conviene nombrar con precisión: request, la petición entrante; env, el objeto con tus bindings; y ctx, el contexto de ejecución con utilidades de ciclo de vida como waitUntil.

export default {
  async fetch(request, env, ctx): Promise<Response> {
    // request: lo que entra. env: tus capacidades. ctx: el ciclo de vida.
    return new Response('Hola desde el edge');
  },
} satisfies ExportedHandler<Env>;

Ese segundo parámetro no lo construyes tú ni lo importas: te lo entrega la plataforma en cada invocación. Cuando el runtime instancia tu Worker, lee los bindings que declaraste, resuelve cada uno a su cliente correspondiente y los deposita como propiedades de un único objeto. Ese objeto es env. Todos los demás handlers —scheduled para los cron, queue para los consumidores de colas— reciben también su env, por la misma razón y con el mismo contenido.

export default {
  // el cron recibe env en la misma posicion
  async scheduled(controller, env, ctx): Promise<void> {
    await env.MI_KV.put('ultimo-cron', new Date().toISOString());
  },
  // el consumidor de una cola, igual
  async queue(batch, env, ctx): Promise<void> {
    for (const mensaje of batch.messages) {
      await env.DB.prepare('INSERT INTO eventos (dato) VALUES (?)').bind(mensaje.body).run();
    }
  },
} satisfies ExportedHandler<Env>;

No es casualidad que la posición sea siempre la segunda: env es la vía única por la que cualquier punto de entrada del Worker recibe sus capacidades, ya sea una petición HTTP, un disparo de cron o un lote de mensajes de una cola. Aprender a leer un handler es, en buena parte, aprender a leer qué le llega por env.

env es un mapa de capacidades, no de cadenas

La tentación, si vienes de Node, es leer env como un primo de process.env: un diccionario de cadenas de texto. No lo es. Cada propiedad de env es del tipo que le corresponde a su recurso. env.MI_KV es un KVNamespace con métodos como get y put; env.DB es un D1Database con prepare; env.AI es un cliente de inferencia. Solo los bindings de variables y secretos son cadenas; el resto son objetos con comportamiento.

export default {
  async fetch(request, env, ctx): Promise<Response> {
    // cada propiedad de env es un cliente vivo del tipo del recurso
    const sesion = await env.MI_KV.get('sesion:123');
    const { results } = await env.DB.prepare('SELECT * FROM tareas').all();
    const modo = env.MODO; // esto si es una cadena: una var o un secreto

    return Response.json({ sesion, tareas: results, modo });
  },
} satisfies ExportedHandler<Env>;

Accedes a cada capacidad con notación de punto y el nombre lógico que elegiste al declararla. No hay paso de inicialización, no hay conectar(), no hay pool que abrir: la propiedad ya está lista. El objeto env es, además, estable dentro de un mismo isolate: la plataforma lo construye una vez al arrancar el isolate y lo reutiliza en las sucesivas peticiones que ese isolate atienda.

Esa estabilidad tiene una cara y una cruz. La cara: leer un binding no cuesta nada y puedes hacerlo cuantas veces quieras sin reconectar. La cruz: como el mismo env —y el mismo ámbito de módulo— se comparte entre las peticiones que atiende un isolate, nunca guardes en él datos de una petición concreta. Escribir el usuario autenticado de una petición en una variable global y encontrártelo en la siguiente es el error clásico de quien llega con hábitos de un servidor que dedica un proceso a cada petición.

⚠️
env es para capacidades, no para estado de petición

El objeto env y las variables de nivel de módulo viven tanto como viva el isolate, y un isolate atiende muchas peticiones de muchos usuarios distintos. Todo lo que sea específico de una petición —el usuario, un identificador de sesión, un acumulador— debe vivir dentro del handler, en su propio ámbito. Trata env como de solo lectura: es un conjunto de capacidades compartidas, no un cajón donde dejar tus datos entre invocaciones.

📝
env no es process.env

Aunque uses el flag nodejs_compat y exista un process.env, no lo confundas con el env del Worker. process.env es un mecanismo de compatibilidad que expone cadenas; env es el objeto de bindings de la plataforma, y en él viven los clientes de KV, R2, D1 y demás. La fuente de la verdad de tus capacidades es siempre el env que recibe el handler, no una variable global heredada del ecosistema de Node.

Alcanzar env fuera del handler

A veces necesitas un binding antes de que llegue una petición: para inicializar un cliente de una API con una clave, o para leer un nivel de log al arrancar. Como env es un argumento del handler, no está disponible en el ámbito global por defecto. La solución moderna es importarlo directamente desde el módulo cloudflare:workers, lo que te da acceso a los bindings desde cualquier parte del código, incluido el nivel superior del módulo.

import { env } from 'cloudflare:workers';

// se lee al arrancar el isolate, antes de cualquier peticion
const nivelLog = env.NIVEL_LOG ?? 'info';

export default {
  async fetch(request, env, ctx): Promise<Response> {
    return new Response(`Nivel de log: ${nivelLog}`);
  },
} satisfies ExportedHandler<Env>;

Hay una regla que no puedes saltarte: un Worker no permite operaciones de entrada y salida fuera del contexto de una petición. Por eso, aunque env sea accesible en el nivel superior, ahí solo puedes leer valores planos —variables y secretos—. Llamar a env.MI_KV.get(...), invocar un método de un Durable Object o llamar a otro Worker desde el ámbito global fallará: esas acciones son entrada y salida y exigen estar dentro de un handler.

⚠️
Leer un secreto sí; hacer I/O no

En el nivel superior del módulo puedes leer env.MI_SECRETO o env.MODO, porque son solo valores en memoria. Pero no puedes tocar la red ni el almacenamiento: env.MI_KV.get, env.DB.prepare(...).all() o una llamada por service binding solo funcionan dentro de un handler, donde existe el contexto de petición. Usa el env global para configurar, no para actuar.

En las clases que la plataforma instancia por ti —un WorkerEntrypoint, un DurableObject, un Workflow—, el mismo env aparece como propiedad de la instancia: lo lees con this.env. Es la misma llave, entregada por la vía que corresponde a un objeto con estado en lugar de a una función suelta.

flowchart LR
CFG[bindings declarados] --> INST[la plataforma instancia el isolate]
INST --> ENV[construye el objeto env con clientes vivos]
ENV --> H[env llega como segundo argumento del handler]
ENV --> G[import de cloudflare workers para el ambito global]
ENV --> C[this.env en clases con estado]
style ENV fill:#89b4fa,color:#11111b
style H fill:#a6e3a1,color:#11111b
env es la costura entre la infraestructura y el código

El objeto env parece un detalle de la firma de una función, pero es la junta exacta donde la infraestructura entra en tu programa, y su diseño encierra tres decisiones que vale la pena leer despacio. La primera es la inyección: tu código no busca sus dependencias —no lee un fichero, no consulta un servicio de descubrimiento, no arma una conexión— sino que las recibe ya resueltas. Eso es inyección de dependencias elevada al nivel de la plataforma, y tiene la misma virtud que en el diseño de software: tu Worker declara qué necesita y permanece ignorante de cómo se satisface, lo que lo hace verificable y portátil entre entornos. La segunda es que env sea un argumento y no un global: al llegar por parámetro, las capacidades tienen un origen explícito y un alcance acotado a la invocación, en las antípodas de la autoridad ambiental que hace tan peligrosas las variables de entorno tradicionales. Que además exista el import de cloudflare:workers no contradice esto, sino que lo matiza con precisión quirúrgica: te deja leer configuración en el arranque, pero te prohíbe hacer I/O fuera de una petición, porque en el edge no hay un proceso de larga vida esperando en segundo plano, solo isolates que despiertan para atender y se apagan. La tercera decisión es la homogeneidad: una KV, una base de datos D1, otro Worker y un modelo de IA se alcanzan todos igual, como propiedades de un mismo objeto tipado. Esa uniformidad no es estética: significa que añadir una capacidad a un Worker es siempre el mismo gesto —declarar el binding, leer env.LO_QUE_SEA— sin importar la naturaleza del recurso. Cuando interiorizas que env es el punto único por donde tu función pura del edge toca el mundo, dejas de verlo como un parámetro más y empiezas a leerlo como el contrato completo entre tu código y la plataforma.

⚔️ Recorre el llavero
  1. En un Worker con al menos un binding, imprime Object.keys(env) dentro de fetch y confirma que aparecen los nombres lógicos que declaraste.
  2. Lee un valor de un binding de datos —env.MI_KV.get(...) o env.DB.prepare(...)— y observa que no hiciste ningún paso de conexión previo.
  3. Mueve la lectura de una variable de configuración al nivel superior del módulo usando el import de cloudflare:workers; comprueba que compila.
  4. Intenta ahora hacer env.MI_KV.get(...) en ese mismo nivel superior y razona, a partir del error, por qué el I/O exige el contexto de una petición.