APIs web estándar: por qué no es Node.js
El runtime de Workers no implementa las APIs de Node sino las del estándar web: fetch, Request, Response, URL, Web Crypto y streams. El vocabulario del navegador en el servidor, y la apuesta de interoperabilidad de WinterCG detrás.
Un Worker no importa http ni fs; recibe un Request y devuelve un Response, exactamente los mismos objetos que usa fetch en el navegador. El runtime de Cloudflare no habla el dialecto de Node: habla el estándar de la plataforma web. Esa elección —globales tipo navegador en lugar de módulos de Node— no es un capricho, es una apuesta deliberada por la interoperabilidad entre runtimes que hoy comparten Deno, Bun y los propios navegadores.
- Entender el contrato del module worker: entra un
Request, sale unResponse. - Conocer el catálogo de APIs web del runtime:
URL, Web Crypto y streams. - Usar
crypto.subtley los streams sin bufferizar cuerpos enteros en memoria. - Explicar por qué Workers NO es Node.js y qué se gana con esa decisión.
El contrato: request entra, response sale
El formato moderno de un Worker es el module worker: exportas un objeto por defecto con un método fetch. Ese método recibe tres cosas —la petición, el entorno con los bindings, y un contexto de ejecución— y debe devolver una respuesta.
export default {
async fetch(request: Request, env: Env, ctx: ExecutionContext): Promise<Response> {
const url = new URL(request.url);
if (url.pathname === "/salud") {
return new Response("ok", { status: 200 });
}
return new Response("no encontrado", { status: 404 });
},
} satisfies ExportedHandler<Env>;
Request, Response, Headers y URL no son invenciones de Cloudflare: son las interfaces del estándar Fetch y del estándar URL de WHATWG, las mismas que expone el navegador. Un Worker es, literalmente, un manejador de fetch del lado del servidor. Esa simetría es profunda: el mismo objeto Request que construyes para llamar a una API externa es el que tu Worker recibe de sus clientes, y el Response que devuelves es el que otro fetch consumiría. Cliente y servidor dejan de ser dos mundos con vocabularios distintos.
Cloudflare añade a Request una sola extensión propia: el objeto cf, que trae metadatos del edge como el país de origen o el datacenter. Es un superconjunto compatible, no una desviación del estándar: si ignoras cf, tu código es indistinguible del que correría en un navegador. Esa disciplina de “extender sin romper” es lo que mantiene la promesa de portabilidad intacta.
El tercer argumento, ctx, merece una palabra. Expone ctx.waitUntil, que prolonga la vida del Worker más allá de la respuesta para terminar trabajo en segundo plano —escribir un log, poblar una caché— sin hacer esperar al cliente. Es la forma que da el runtime a una idea sencilla: responder ya y seguir un instante después. En un servidor tradicional lo resolverías dejando el proceso vivo; aquí, sin proceso persistente, hace falta una primitiva explícita para ello.
El catálogo de APIs web
fetch, Request, Response
El estándar Fetch completo. Haces peticiones salientes con fetch y respondes construyendo un Response. El mismo vocabulario para cliente y servidor.
URL y URLSearchParams
Parseo conforme al estándar WHATWG. Nada de partir cadenas a mano: new URL(request.url) te da host, pathname y query listos para usar.
Web Crypto
crypto.subtle para hashing, HMAC, firma y cifrado; más crypto.randomUUID y crypto.getRandomValues. Asíncrono y basado en promesas.
Streams
ReadableStream, WritableStream y TransformStream del estándar WHATWG Streams. Procesas cuerpos enormes sin cargarlos enteros en memoria.
A esos se suma un buen puñado de utilidades globales que reconocerás del navegador:
TextEncoderyTextDecoderpara convertir entre texto y bytes.structuredClonepara clonar estructuras profundas sin serializar a JSON.atobybtoapara Base64, yAbortControllerpara cancelar operaciones.- Los timers
setTimeoutyqueueMicrotask, más el subconjunto habitual deconsole.
La regla mental es simple: si existe en un navegador moderno y no depende del DOM, es muy probable que exista también aquí. No hay window ni document, porque no hay página; todo lo demás de la plataforma web tiende a estar presente y a comportarse igual.
Hay un matiz del estándar que sorprende a quien llega de Node: el Request entrante y sus Headers son inmutables. No puedes mutar las cabeceras que recibes; si quieres modificarlas, construyes un Request o un Response nuevo a partir del original. No es una limitación de Cloudflare, sino el contrato del estándar Fetch, y respetarlo es parte de escribir código que se comporte igual en los cuatro runtimes.
Web Crypto y streams en la práctica
Web Crypto es asíncrono y se organiza por nombres de algoritmo como cadenas. Calcular un SHA-256 de un texto, por ejemplo, devuelve una promesa y no bloquea el isolate:
async function sha256Hex(texto: string): Promise<string> {
const datos = new TextEncoder().encode(texto);
const hash = await crypto.subtle.digest("SHA-256", datos);
return [...new Uint8Array(hash)]
.map((b) => b.toString(16).padStart(2, "0"))
.join("");
}
Que la API sea asíncrona no es un capricho: muchas operaciones criptográficas se apoyan en implementaciones nativas y, en algunos entornos, en aceleración por hardware, y el contrato basado en promesas permite exponerlas sin bloquear el bucle de eventos. Es una API distinta de la de Node —nombres de algoritmo como cadenas, claves como objetos opacos CryptoKey— y esa diferencia es intencionada: es la forma estandarizada, la misma en el navegador.
Un caso real donde brilla es la verificación de webhooks, tan común en el edge. Comprobar una firma HMAC contra el cuerpo recibido es una operación de una línea, y crypto.subtle.verify la resuelve en tiempo constante para no filtrar información por temporización:
async function firmaValida(clave: CryptoKey, firma: ArrayBuffer, cuerpo: string) {
const datos = new TextEncoder().encode(cuerpo);
return crypto.subtle.verify("HMAC", clave, firma, datos);
}
Los streams son la otra pieza esencial en el edge, donde la memoria por isolate es limitada. Un TransformStream te permite transformar un cuerpo mientras pasa, sin bufferizarlo entero, y empezar a responder antes de haber leído toda la entrada:
export default {
async fetch(): Promise<Response> {
const upstream = await fetch("https://ejemplo.com/grande.txt");
const { readable, writable } = new TransformStream();
upstream.body?.pipeTo(writable);
return new Response(readable, { headers: { "content-type": "text/plain" } });
},
};
Ese patrón —leer de un origen y responder por un stream— procesa gigabytes con una huella de memoria de kilobytes, y empieza a enviar bytes al cliente de inmediato. Es la diferencia entre proxiar como un servidor tradicional, que acumula el cuerpo entero, y proxiar como un runtime del edge, que fluye.
En Workers, el cuerpo de un Request o un Response es por defecto un ReadableStream. Puedes consumirlo cómodamente con await request.text() o await request.json(), pero eso bufferiza todo en memoria. Cuando el cuerpo puede ser grande, trátalo como el stream que es: encadénalo con pipeTo o pipeThrough y deja que fluya. Pensar en cuerpos como cadenas es un hábito de Node que en el edge cuesta memoria y latencia.
Por qué NO es Node.js
Node definió su propio universo de APIs en 2009, antes de que la plataforma web tuviera fetch, streams o Web Crypto: require, Buffer, http.createServer, fs. Workers nació quince años después, cuando el estándar web ya ofrecía equivalentes, y eligió esos en su lugar. No hay require (solo import de ESM), no hay Buffer por defecto (usas Uint8Array), no hay fs, y process está muy reducido.
Detrás de esta elección hay una iniciativa concreta: WinterCG, hoy continuada como el grupo técnico WinterTC dentro de Ecma. Su objetivo es definir el conjunto mínimo común de APIs web que todo runtime del lado del servidor debe implementar igual. Cuando escribes contra ese mínimo común, tu código es portable: el mismo manejador de fetch corre sin cambios en Workers, en Deno, en Bun y en un navegador.
flowchart LR A[tu codigo contra el minimo comun web] --> B[Cloudflare Workers] A --> C[Deno] A --> D[Bun] A --> E[Navegador]
Esto no significa que Node desaparezca ni que estuviera equivocado: en 2009, la plataforma web no ofrecía nada de esto, y Node tuvo que inventar su propio vocabulario para existir. La diferencia es temporal, no de mérito. Los runtimes que nacieron después heredaron un estándar web ya maduro y, con buen criterio, se apoyaron en él en lugar de reinventar otro dialecto propietario. El siguiente nivel de esta guía muestra el puente que Cloudflare tiende hacia el ecosistema de Node cuando una librería lo necesita.
Elegir el estándar web en lugar de las APIs de Node parece un detalle técnico, pero es una tesis sobre el futuro del cómputo del lado del servidor. Node fue, durante una década, el estándar de facto: escribir para el servidor significaba escribir para Node, con sus Buffer y sus módulos propios, y ese código quedaba atado a un runtime concreto. Cloudflare, Deno y Bun apostaron por lo contrario —que el vocabulario del navegador, ya estandarizado y ya conocido por millones de desarrolladores, debería ser también el del servidor— y crearon WinterCG para formalizarlo. La consecuencia es que fetch, Request, Response, URL, crypto.subtle y los streams se vuelven una lengua franca: aprendes un vocabulario una sola vez y lo usas en el cliente y en el servidor, en cualquier runtime que respete el mínimo común. Y esto invierte el diagnóstico habitual: cuando una librería asume Node y no arranca en Workers, la conclusión intuitiva —“al runtime le falta algo”— casi siempre es errónea. Lo que ocurre es que esa librería se escribió contra un dialecto propietario en lugar de contra la plataforma. La pregunta deja de ser qué le falta a Workers y pasa a ser por qué ese código no se escribió contra el estándar que ya comparten cuatro runtimes distintos.
- Escribe un module worker que devuelva JSON distinto según el
pathname, usandonew URL(request.url)para enrutar. - Implementa
sha256Hexconcrypto.subtle.digesty verifica que el resultado coincide con el de una herramienta externa. - Proxia un recurso grande con un
TransformStreamy comprueba que la respuesta empieza a llegar antes de que termine la descarga del origen. - Toma un fragmento que use
Bufferorequirey reescríbelo conUint8Arrayeimport; explica qué dialecto asumía el original.