Assets y Worker juntos: el fallthrough y run_worker_first
Servir estáticos y ejecutar código desde un mismo Worker: el orden por defecto en que una petición intenta primero un asset y solo cae en el código si no lo encuentra, el binding `ASSETS` para servir archivos de forma programática, y `run_worker_first` para invertir el orden y ejecutar el Worker antes que los assets en las rutas que elijas.
Un sitio real rara vez es solo estático: tiene un front-end de archivos que se sirven tal cual y, a la vez, una API o un renderizado en servidor que exige código. La pregunta de arquitectura es quién decide, en cada petición, si toca servir un byte inerte o ejecutar lógica. Cuando pones assets y main en el mismo manifiesto, esa decisión sigue un orden preciso que puedes leer y, cuando hace falta, invertir. Dominar ese orden —el fallthrough de asset a código y su reverso, run_worker_first— es lo que te permite servir el HTML, el CSS, la API y el SSR desde un único Worker sin ambigüedad sobre qué se ejecuta cuándo.
- Entender el orden por defecto: primero se intenta un asset, y solo si no existe cae en el código.
- Usar el binding
ASSETSpara servir archivos de forma programática conenv.ASSETS.fetch. - Invertir el orden con
run_worker_firstpara ejecutar el Worker antes que los assets. - Combinar ambos para servir front-end estático y una API desde un mismo Worker.
El fallthrough: asset primero, código después
Cuando un manifiesto declara a la vez un bloque assets con un directorio y un main con código, cada petición sigue un orden por defecto que conviene grabar. El edge primero comprueba si la ruta coincide con un asset del directorio. Si coincide, sirve ese archivo y el Worker no llega a ejecutarse —lo que mantiene esa petición gratuita—. Solo cuando no hay ningún asset para esa ruta, la petición “cae” hacia tu código: se invoca el handler fetch de tu Worker. A eso se le llama el fallthrough, y es la razón por la que tu API convive con tu front-end sin que tengas que enrutar los estáticos a mano.
{
"name": "mi-app",
"main": "src/index.ts",
"compatibility_date": "2026-03-01",
"assets": {
"directory": "./dist",
"binding": "ASSETS"
}
}
Con este manifiesto, una petición a /estilos.css la resuelve el asset y tu código ni se entera; una petición a /api/usuarios, que no tiene archivo, cae en el Worker. Tu handler, por tanto, solo necesita ocuparse de lo que no es estático:
export default {
async fetch(request, env) {
const url = new URL(request.url);
if (url.pathname.startsWith("/api/")) {
return Response.json({ usuarios: [] });
}
return new Response("No encontrado", { status: 404 });
},
};
flowchart TD
R[Peticion entrante] --> A{Coincide un asset}
A -->|si| S[Sirve el archivo estatico gratis]
A -->|no| W[Cae en el handler fetch del Worker]
W --> API[El codigo responde la API o el SSR]
style S fill:#a6e3a1,color:#11111b
style W fill:#89b4fa,color:#11111bEl binding ASSETS: servir archivos desde el código
El campo binding del bloque assets expone env.ASSETS, un objeto con un método fetch que te deja pedir un asset desde dentro del Worker. Esto es útil cuando el código necesita decidir algo antes de entregar un archivo: comprobar una sesión antes de servir una página privada, o inyectar contenido dinámico en una plantilla estática. En lugar de leer el archivo tú mismo, delegas en env.ASSETS.fetch y recibes la respuesta que el edge habría servido:
export default {
async fetch(request, env) {
const url = new URL(request.url);
if (url.pathname.startsWith("/privado/")) {
if (!estaAutenticado(request)) {
return new Response("Prohibido", { status: 403 });
}
}
// Delega el resto en la maquinaria de assets
return env.ASSETS.fetch(request);
},
};
Invertir el orden con run_worker_first
El fallthrough por defecto es asset primero, código después. Pero a veces necesitas lo contrario: que el Worker se ejecute antes de que se intente ningún asset, para poder interceptar la petición —autenticar, reescribir, registrar, hacer un test A/B— incluso en rutas que sí tienen un archivo detrás. Para eso está run_worker_first. En su forma más simple es un booleano que invierte el orden para todas las rutas; en su forma potente es una lista de patrones que te deja aplicar la inversión con cirugía:
{
"assets": {
"directory": "./dist",
"binding": "ASSETS",
"run_worker_first": ["/api/*", "/privado/*", "!/privado/logo.png"]
}
}
Esa lista dice: ejecuta el Worker primero para todo lo que empiece por /api/ y /privado/, salvo /privado/logo.png, que se sirve directo como asset. El prefijo ! niega un patrón. Fuera de esas rutas, se mantiene el orden normal de asset primero. Así reservas el coste de invocar código solo para las rutas que de verdad lo necesitan, y dejas que el resto del sitio se sirva gratis desde el edge.
La regla práctica es simple. Si una ruta se sirve casi siempre tal cual y solo rara vez necesita lógica, déjala en el orden por defecto: se servirá gratis desde el asset y solo caerá en código en el hueco. Si una ruta necesita que tu Worker la vea siempre —porque hay que autenticar, medir o reescribir en cada visita—, ponla en run_worker_first. No inviertas el orden globalmente por comodidad: cada ruta que pasa por el Worker deja de ser gratuita, así que la lista de patrones es también una decisión de coste, no solo de comportamiento.
Poner assets y código en el mismo Worker disuelve una separación que la industria trató durante décadas como si fuera una ley de la física: la que hay entre el servidor web que sirve archivos y el servidor de aplicaciones que ejecuta lógica. Apache o Nginx delante, tu aplicación detrás; el CDN por un lado, el back-end por otro. Esa arquitectura de dos piezas existía porque servir archivos y computar respuestas tenían perfiles de recurso tan distintos que convenía darles máquinas, procesos y equipos separados. En el edge esa distinción se desvanece: no hay dos servidores, hay un único punto de entrada que, ante cada petición, decide si la respuesta ya está calculada —y entonces la entrega como bytes, gratis— o si hay que calcularla —y entonces arranca un isolate—. El fallthrough y run_worker_first son, vistos así, los dos sentidos de una misma perilla: cuánta autoridad le das a tu código sobre cada ruta. En un extremo, el código solo ve lo que ningún archivo pudo responder, y tu sitio es casi enteramente estático con islas de dinamismo. En el otro, el código ve toda petición antes que nadie, y tu sitio es una aplicación que a veces resulta que devuelve archivos. Entre ambos extremos hay un continuo que tú calibras ruta por ruta, y esa calibración no es un detalle técnico: es la arquitectura de tu aplicación expresada como una lista de patrones. Lo que antes eran dos sistemas con un contrato de red entre ellos es ahora una sola función que enruta entre dos formas de responder. El ingeniero que entiende esto deja de preguntarse “¿esto va en el CDN o en el back-end?” y empieza a preguntarse “¿esta ruta se sirve o se computa, y quién debe decidirlo primero?”.
- Crea un manifiesto con
main, un bloqueassetscondirectoryybinding: "ASSETS", y un front-end estático en el directorio. - Escribe un handler
fetchque responda una API en JSON bajo/api/y verifica que/index.htmllo sirve el asset sin invocar tu código. - Añade una ruta
/privado/que compruebe una cabecera de autenticación y, si es válida, delegue enenv.ASSETS.fetchpara servir la página protegida. - Configura
run_worker_firstcomo lista para que solo/api/*y/privado/*pasen primero por el Worker, y confirma que el resto del sitio se sigue sirviendo como assets. - Añade un patrón negado con
!para exceptuar un archivo concreto dentro de/privado/y razona por qué esa exención lo devuelve al camino gratuito de los assets.