wandres.dev
WRANGLER AVANZADO · entornos y CI/CD

Monorepo y proyectos grandes: muchos Workers, un repositorio

Organizar varios Workers en un mismo repositorio: un manifiesto por unidad desplegable, configuración y tipos compartidos sin herencia mágica, desarrollo local con varios Workers a la vez, y pipelines que detectan qué cambió para desplegar solo eso.

⏱ 20 min

Ningún sistema serio termina siendo un solo Worker. Aparece la API, el frontend con renderizado en el servidor, el consumidor de la cola, el cron que reconcilia, el servicio de imágenes. Ponerlos todos en un repositorio es la decisión correcta la mayoría de las veces, porque mantiene un único historial, un único conjunto de tipos compartidos y la posibilidad de cambiar un contrato y a sus dos lados en el mismo commit. Pero un monorepo mal montado paga esa comodidad con un peaje diario: cada push reconstruye todo, cada despliegue publica servicios que nadie tocó, y la configuración se duplica hasta que nadie sabe por qué un Worker tiene una fecha de compatibilidad distinta de su vecino. Esta lección trata de la disciplina que hace que un repositorio con doce Workers se sienta ligero, y su núcleo es una idea sencilla: saber con precisión qué cambió.

🎯 Al terminar esta lección sabrás
  • Estructurar un repositorio con un manifiesto de Wrangler por unidad desplegable.
  • Compartir código, tipos y convenciones de configuración sin depender de herencia inexistente.
  • Levantar varios Workers a la vez en desarrollo y probar los service bindings de verdad.
  • Construir un pipeline que detecte los proyectos afectados y despliegue solo esos.

Un manifiesto por unidad desplegable

La unidad de despliegue en Cloudflare es el Worker, así que la estructura del repositorio debe reflejarla: cada Worker vive en su carpeta, con su wrangler.jsonc, su package.json y su código. El código común se extrae a paquetes del workspace que ningún manifiesto conoce, porque son dependencias normales que el bundler resuelve.

apps/
  api/            wrangler.jsonc  package.json  src/index.ts
  web/            wrangler.jsonc  package.json  src/index.ts
  cola-emails/    wrangler.jsonc  package.json  src/index.ts
packages/
  dominio/        tipos y logica de negocio pura
  contratos/      esquemas compartidos entre productor y consumidor
pnpm-workspace.yaml

La pregunta que define la calidad de este diseño no es cuántos Workers tienes sino dónde cortas. Un buen límite coincide con algo que cambia a su propio ritmo, se despliega por separado y falla sin arrastrar a los demás. Cortar por capas técnicas —un Worker para la validación, otro para el acceso a datos— produce servicios acoplados que siempre se despliegan juntos y que solo han añadido latencia de red donde antes había una llamada a función. Cortar demasiado tarde produce el Worker monolito, cuyo bundle crece hasta rozar los límites de tamaño y cuyo despliegue es siempre arriesgado porque toca todo.

💡
Un Worker se justifica por su ciclo de vida, no por su tamaño

Antes de crear un Worker nuevo, responde a tres preguntas: ¿se despliega en momentos distintos que el resto?, ¿escala en una dimensión distinta?, ¿puede caerse sin que caiga lo demás? Si las tres respuestas son negativas, lo que tienes es un módulo, y un módulo se importa. La red entre dos Workers de Cloudflare es rapidísima, pero no es gratis en complejidad: añade un contrato que versionar, un fallo parcial que manejar y un despliegue más que coordinar.

Configuración compartida sin herencia mágica

Aquí conviene ser directo: los manifiestos de Wrangler no se extienden unos de otros. No hay una clave que importe la configuración del padre, y buscarla lleva a inventar generadores frágiles. Lo que sí funciona es aceptar la duplicación deliberada de lo poco que de verdad debe coincidir y automatizar su verificación.

Qué compartir Cómo
Fecha y flags de compatibilidad Duplicados en cada manifiesto, con un test o un script de lint que falle si divergen
Tipos de env wrangler types por Worker, generando su propio archivo de declaración junto a su código
Lógica de negocio Un paquete del workspace importado como dependencia normal
Esquemas de mensajes y respuestas Un paquete de contratos que importan a la vez el productor y el consumidor
Convenciones de nombres y entornos Documentadas y verificadas en CI, no heredadas

La deriva de la fecha de compatibilidad es el problema silencioso más común de un monorepo grande: cada Worker se creó en un momento distinto, nadie las actualiza a la vez, y al cabo de un año tienes servicios corriendo con semánticas de runtime diferentes que se comunican entre sí. Un script de veinte líneas que lea todos los manifiestos y compare ese campo cuesta menos que el primer incidente que causa.

Para los tipos, la regla es que cada Worker genere los suyos con wrangler types a partir de su propio manifiesto. Compartir un único tipo de env entre Workers con bindings distintos es tentador y es un error: convierte en compilable un código que accede a un binding que ese Worker no tiene, y el fallo aparece en producción como una propiedad indefinida.

flowchart TD
R[repositorio] --> A[apps api]
R --> W[apps web]
R --> Q[apps cola emails]
R --> P[packages dominio y contratos]
P --> A
P --> W
P --> Q
W -->|service binding| A
A -->|queue| Q
style P fill:#a6e3a1,color:#11111b

Desplegar solo lo que cambió

Este es el corazón operativo del monorepo. La estrategia tiene dos niveles y conviene entender que resuelven cosas distintas.

El primero filtra a la entrada: el pipeline decide qué proyectos se ven afectados por el diff antes de construir nada. Con una herramienta de orquestación es directo, porque conoce el grafo de dependencias y sabe que un cambio en el paquete de contratos afecta al productor y al consumidor aunque sus carpetas no se hayan tocado.

# Solo los proyectos afectados por el diff contra la rama principal
pnpm turbo run deploy --filter="...[origin/main]"

El segundo nivel filtra por rutas explícitas, y es lo que ofrecen tanto los disparadores de GitHub Actions como las rutas vigiladas de Workers Builds. Es más simple y más frágil: solo ve carpetas, así que un cambio en un paquete compartido no dispara los Workers que dependen de él salvo que incluyas esa ruta en cada filtro.

jobs:
  detectar:
    runs-on: ubuntu-latest
    outputs:
      afectados: ${{ steps.filtro.outputs.changes }}
    steps:
      - uses: actions/checkout@v4
        with: { fetch-depth: 0 }
      - id: filtro
        uses: dorny/paths-filter@v3
        with:
          filters: |
            api:
              - 'apps/api/**'
              - 'packages/**'
            web:
              - 'apps/web/**'
              - 'packages/**'

  desplegar:
    needs: detectar
    if: needs.detectar.outputs.afectados != '[]'
    strategy:
      matrix:
        proyecto: ${{ fromJSON(needs.detectar.outputs.afectados) }}
    runs-on: ubuntu-latest
    environment: production
    steps:
      - uses: actions/checkout@v4
      - run: npx wrangler deploy
        working-directory: apps/${{ matrix.proyecto }}
        env:
          CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }}

La matriz dinámica es el patrón que hace esto escalable: la lista de proyectos afectados se calcula una vez y se convierte en tantos jobs paralelos como haga falta. Con fetch-depth en cero para que el diff tenga historia suficiente, y con la precaución de incluir siempre los paquetes compartidos en cada filtro.

⚠️
Desplegar de menos es peor que desplegar de más

Un filtro demasiado agresivo produce el fallo más difícil de diagnosticar del monorepo: un cambio en un contrato compartido se despliega en el productor pero no en el consumidor, porque el filtro de este último no incluía esa ruta. El sistema queda temporalmente inconsistente y el error aparece lejos de la causa. Ante la duda, incluye de más: el coste de un despliegue innecesario es un minuto de pipeline y una versión nueva sin cambios funcionales; el coste de uno omitido es un incidente. Y diseña los contratos para que un despliegue parcial sea tolerable, porque tarde o temprano ocurrirá.

Desarrollo local y contratos entre Workers

Un monorepo que solo se puede probar desplegando no sirve. Wrangler permite levantar varios Workers en una única sesión pasando varios manifiestos, y en ese modo los service bindings entre ellos se resuelven contra los procesos locales en lugar de contra los desplegados.

wrangler dev -c apps/web/wrangler.jsonc -c apps/api/wrangler.jsonc

Eso convierte la composición entre servicios en algo comprobable en tu máquina, con recarga en caliente de ambos lados. Es la diferencia entre poder cambiar un contrato y su consumidor en el mismo commit con confianza, o descubrir el desajuste en staging media hora después.

Los contratos entre Workers del mismo repositorio tienen una trampa que conviene nombrar. Que compartan repositorio y tipos crea la ilusión de que se despliegan atómicamente, y no es así: son artefactos independientes que se publican en momentos distintos, con segundos u horas de diferencia. La misma disciplina que aplicarías entre servicios de equipos distintos —añadir campos antes de exigirlos, tolerar los desconocidos, retirar lo viejo en un despliegue posterior— sigue siendo obligatoria. El monorepo elimina el problema de coordinar el código, no el de coordinar el tiempo.

El monorepo no unifica el despliegue: unifica la verdad sobre el sistema

La razón por la que los equipos adoptan monorepos casi nunca es la que declaran. Se dice que es para simplificar el despliegue, y es justo al revés: desplegar desde un monorepo es estrictamente más complicado que desde repositorios separados, porque tienes que resolver el problema de qué cambió, que en repositorios separados no existe. Lo que un monorepo te da de verdad es otra cosa, mucho más valiosa y más difícil de conseguir: en cualquier instante existe un commit que describe el estado completo y coherente de todo tu sistema. Puedes hacer un cambio que atraviese cinco servicios y revisarlo como una sola unidad. Puedes cambiar el nombre de un campo en el paquete de contratos y que el compilador te enseñe, de una vez, los siete sitios que se rompen. Puedes hacer una bisección sobre el historial de un sistema entero en lugar de sobre el de una pieza. Esa capacidad —razonar sobre el sistema como un todo— es lo que compras, y el precio es exactamente el trabajo de tooling que esta lección describe. Quien adopta el monorepo por comodidad y no invierte en detección de afectados, en caché y en verificación de la coherencia entre manifiestos, acaba con lo peor de ambos mundos: la complejidad del despliegue múltiple sin el beneficio de la vista unificada, porque nadie se atreve ya a tocar los paquetes compartidos. La pregunta de diseño no es cuántos repositorios quieres, es dónde prefieres pagar la coordinación: en el momento del cambio, revisando un diff grande que abarca todo, o en el momento del incidente, reconstruyendo qué versión de cada servicio estaba viva cuando falló.

⚔️ Haz que tu repositorio sepa qué cambió
  1. Reorganiza un proyecto con dos Workers en carpetas separadas, cada uno con su manifiesto, y extrae a un paquete compartido el tipo de la respuesta que se intercambian.
  2. Levanta ambos con una sola sesión de wrangler dev pasando los dos manifiestos y comprueba que el service binding resuelve contra el proceso local.
  3. Escribe un script que lea todos los manifiestos del repositorio y falle si sus fechas de compatibilidad no coinciden. Conéctalo al pipeline.
  4. Monta un job de detección de afectados con matriz dinámica y comprueba que un cambio solo en un Worker despliega uno y no dos.
  5. Rompe deliberadamente el filtro: cambia el paquete compartido sin incluirlo en los patrones y observa el despliegue parcial. Después arréglalo y documenta por qué el contrato debe tolerar ese estado.