¿Qué es un monorepo y cuándo estructurar así tu código?
El desarrollo de aplicaciones cada vez es más complejo y para mitigar todas esa complejidad, los desarrolladores tenemos herramientas y arquitecturas que permiten una organización más cómoda y versátil del código de los proyectos. De esta manera, conseguimos mejorar la experiencia de desarrollo y la colaboración entre los equipos de trabajo. Uno de los ejemplos son los mono-repositorios (o como se conocen habitualmente monorepos), algo que resultará útil tanto si trabajas solo como si perteneces a un equipo de trabajo más amplio.
¿Qué es un monorepo?
La idea detrás del monorepo, o repositorio monolítico, es sencilla. Consiste simplemente en agrupar en un único contexto diversos proyectos con aplicaciones, servicios o librerías qué generalmente desarrollarías en carpetas y espacios Git separados. El monorepo simplemente las agrupa dentro de un mismo directorio toda una serie de subproyectos, aunque manteniendo la singularidad de cada elemento, con su propia estructura, en un subdirectorio individual. Es decir, juntos pero no revueltos.
Habitualmente en el monorepo tendrás un único repositorio de Git en el que se encontrarán cada una de las carpetas de los proyectos que estás agrupando. Por tanto, llevarás un único control de versiones que agrupa a todos los subproyectos.
Antes de empezar a detallar de manera más concreta sus características es importante distinguir entre monorepos y aplicaciones monolíticas. Mientras que una aplicación monolítica contiene en un mismo proyecto de software todas las necesidades de funcionalidad de manera agrupada, los monorepos en realidad agrupan diferentes aplicaciones donde cada una de ellas puede tener una responsabilidad muy diferente. Por tanto, el hecho de utilizar un monorepo no indica que vayas a realizar un único proyecto con todo junto. En realidad se trata más de agrupar diversos proyectos en una única carpeta que gestionarás con Git para facilitar los procesos del día a día del desarrollo.
Existen muchos ejemplos de monorepo, y luego veremos algunos, pero para ir concretando las cosas imagina una aplicación con separación completa entre frontend y backend. Podrías organizarlo en proyectos Git completamente separados pero también en un monorepo gestionado desde una carpeta padre que llevas a Git de manera global.
¿Cómo funciona un monorepo?
En principio, el funcionamiento de un monorepo no tiene en realidad mucha complicación. Simplemente consiste en agrupar diversos proyectos en una única carpeta. De todos modos vamos a detallar punto por punto cómo tenemos que organizarlos para que nadie se pierda y para entender los principales retos que tendremos que encarar con esta arquitectura.
Organización de carpetas y estructuración de proyectos y paquetes compartidos
Lo primero que debemos entender es que existe una única carpeta global donde vamos a ir colocando en subcarpetas independientes cada uno de los proyectos, librerías o paquetes que queramos agrupar.
Cada proyecto agrupado en esta carpeta puede tener características completamente diferentes, usar lenguajes distintos, frameworks diametralmente opuestos, dedicarse a ambientes diferentes, como la parte del frontend o el backend.
Gestión centralizada de dependencias mediante workspaces de gestores de paquetes
Una de las cosas que nos permite realizar un monorepo es agrupar las dependencias mediante workspaces. Esta es una funcionalidad a veces desconocida de gestores de paquetes como npm pero que ya tiene bastante tiempo con nosotros.
Básicamente consiste en definir cada uno de los espacios de trabajo de manera individual en su carpeta independiente, aunque el gestor de paquetes colocará las dependencias en un solo espacio global, a un nivel superior a las carpetas de los subproyectos. Esto simplifica la gestión de dependencias de los proyectos y nos soluciona problemas de consistencia.
Grafo de dependencias y resolución del orden de compilación (build graph)
Trabajar con monorepos trae consigo algunos retos y uno de ellos es la gestión de las dependencias, que afecta directamente al orden de compilación. Por ejemplo, imagina que tienes en un mismo repositorio una librería de UI junto con una app. Si intentas compilar primero la aplicación antes de haber compilado la librería de UI el proceso no funcionará bien, porque los cambios de las interfaces de usuario deberían afectar directamente a la nueva compilación de la aplicación.
Para ello, lo que tenemos que hacer es un grafo de dependencias y utilizarlo a la hora de hacer diversos procesos, como la compilación, de modo que puedas definir el orden de realización de cada paso, que debe respetarse para que funcione todo correctamente.
Caché de compilación local y remota para acelerar la ejecución de tareas
También es interesante contar con herramientas de caché para optimizar los procesos de compilación. Este caché puede estar perfectamente en local o en remoto, incluso ser una caché distribuida. Volviendo a nuestro ejemplo, si la UI no ha cambiado desde la última compilación, la herramienta no la vuelve a procesar; simplemente rescata el resultado de la caché y pasa directamente al siguiente nodo del grafo.
Detección de proyectos afectados (affected detection) tras un cambio en el código
Para la detección de los proyectos afectados se utilizan diversas herramientas, siendo Git la primera opción. A través del control de versiones podemos saber qué diferencias hay entre los distintos paquetes de código, para saber si se tienen que compilar o no.
Además, las herramientas de gestión de repositorios utilizan otras técnicas para saber si realmente los cambios detectados han afectado las compilaciones. La más recurrida es el «hasheo», que permite saber si el build ha sido alterado o no, incluso cuando se han detectad cambios en el código. Esto es importante porque si no hubo cambios, entonces se pueden utilizar los archivos producidos en anteriores compilaciones desde la caché.
¿Cuándo estructurar tu código con monorepo?
En los próximos puntos vamos a ver cuáles son los casos de uso más frecuentes que te podrían llevar a estructurar tu proyecto mediante la arquitectura de monorepo.
Equipos que comparten múltiples bibliotecas de componentes e infraestructura
Un primer ejemplo donde puede venir muy bien es cuando hay equipos que comparten múltiples bibliotecas que afectan a un proyecto general, en los cuales puedes tener desde componentes de interfaz gráfica hasta la gestión de la infraestructura.
Proyectos con aplicaciones web y móviles que consumen el mismo backend de utilidades
Puedes utilizarlo también en proyectos que tienen diversos ambientes de despliegue, como por ejemplo, una aplicación web y una aplicación móvil. Podrías tener un mismo backend qué alimenta un proyecto web y una aplicación móvil, colocando tanto el backend como los distintos frontales en el mismo monorepo.
Organizaciones que buscan estandarizar configuraciones de linters, tests y TypeScript
También te puede venir bien cuando simplemente quieres reutilizar y estandarizar las configuraciones de herramientas de análisis del código como los linters, o bien mantener los test unificados en un único lugar, pudiendo testear de una vez varias librerías. Incluso puede ser útil cuando tienes configuraciones de lenguajes como TypeScript que quieres mantener en un único lugar.
Necesidad de refactorizaciones masivas e interconectadas en un único commit atómico
Los monorepos también vienen como anillo al dedo, cuando tenemos que realizar refactorizaciones que afectan a diversos proyectos, en las cuales quieres realizar un único commit que despliegue actualizaciones en varias bases de código.
Escenarios donde la sincronización manual de versiones de paquetes genera cuellos de botella
Otro lugar muy habitual es cuando tienes que desarrollar varios proyectos donde dependes de librerías en común, o distintos proyectos de librerías donde hay dependencias entre ellas que nos fuercen a mantener las versiones sincronizadas.
Gracias a los mono-repositorios este esta gestión de versiones se realiza de manera automática y evita que cometamos fallos a la hora de mantener el software
Ventajas de utilizar monorepo
Ya hemos explicado de un modo general cómo los monorepos consiguen optimizar distintos flujos de trabajo de las aplicaciones. Ahora vamos a ver algunas ventajas más concretas que se obtienen al utilizarlos.
Reutilización directa de código y componentes UI sin necesidad de publicarlos en NPM
Si tienes una librería de componentes de interfaz gráfica separada de tu aplicación unirlas en un mismo repositorio puede facilitar mucho las cosas porque no tendrás que compilar y subir continuamente la librería de UI con cada pequeño cambio que requieras aplicar debido a las necesidades de tu aplicación.
Llevar ambos proyectos integrados te facilitará mucho el trabajo con las versiones. Si estuvieran por separado tendrías que subir las nuevas versiones a npm y actualizar el proyecto que usa la librería. Al estar unidos bajo un mismo monorepo, cualquier cambio en la UI impactará de manera inmediata en la aplicación, evitando muchas idas y venidas. Además, los LLMs tendrán un contexto mucho mayor de lo que estás haciendo.
Refactorizaciones globales sencillas y cambios en transacciones atómicas
Cuando trabajamos con un mono-repositorio, tenemos la ventaja de actuar de una manera global en todo el conjunto de proyectos, en vez de tener que ir tocando cada uno de los proyectos por separado. Esto resulta muy útil porque permite hacer refactorizaciones que afectan a varios proyectos de una única vez y enviarlos al repositorio global de manera atómica, provocando un único despliegue que afecta a todos los proyectos en conjunto.
Esta situación mejora mucho frente a la necesidad de actualizar por separado cada proyecto y desplegar de manera independiente, lo que nos llevaría muchísimos más pasos.
Unificación de dependencias y versiones de librerías para todo el ecosistema
El hecho de trabajar con un único repositorio también nos permite mejorar la gestión de las dependencias, que podremos hacer de manera unificada. Esto no solo implica un ahorro de espacio de disco, sobre todo lo que nos ofrece es una mayor consistencia.
Incluso podemos seguir una política de gestión de monorepos de versionado único. Esto quiere decir que, cuando cambia algo de todo el proyecto cambian las versiones de todos los paquetes incluidos en él. Esta política ayuda mucho para evitar que cada parte de la aplicación utilice los mismos paquetes, evitando inconsistencias o el uso de distintas versiones del mismo componente.
Transparencia y visibilidad total del código fuente entre diferentes equipos
Gracias a un monorepo existe una visibilidad total del código fuente entre los diversos equipos que forman parte del proyecto. Esto ayuda no solo a las personas, sino también a los agentes de inteligencia artificial que consiguen tener un contexto mucho más amplio sobre el proyecto.
Simplificación de la configuración de herramientas de desarrollo y calidad de código
Por último, nos beneficiamos de una simplificación muy notable a la hora de configurar los entornos de desarrollo y las herramientas de análisis del código, para asegurar su calidad. De hecho, podremos utilizar las mismas configuraciones para todos los proyectos, lo que además también mejorará la consistencia, alineando las decisiones entre todos los equipos y proyectos de una misma aplicación
Monorepo vs. Multirepo
La elección entre un monorepo y un multirepo (un repositorio independiente por proyecto o servicio) define no sólo cómo se almacena el código, sino también la dinámica de colaboración de los equipos y las estrategias de publicación. También puede afectar de manera sensible la complejidad de las herramientas de integración continua (CI/CD). Estos son los principales puntos que tenemos que llevar en consideración:
- Enlaces simbólicos directos frente a versiones en registros. En un monorepo, las dependencias internas suelen vincularse localmente mediante symlinks o workspaces (usando herramientas como pnpm, Nx o Lerna). Estas herramientas permiten probar cambios entre bibliotecas al instante sin publicar nada. Este panorama no es tan favorable en multirepo, porque cada cambio en un paquete compartido requiere compilar, aplicar Semantic Versioning, publicar en un registro como npm y actualizar manualmente la versión en los proyectos consumidores. Esto aísla los entornos, pero añade pasos intermedios qué pueden ser bastante tediosos en el día a día de los desarrolladores
- Ejecución condicional basada en cambios frente a pipelines aislados. Los monorepos modernos utilizan análisis de grafos de dependencias para ejecutar pruebas y despliegues únicamente en las partes afectadas por un commit. Esto puede no parecer tan importante, pero sí tiene connotaciones que merece la pena tener en cuenta, como el ahorro de minutos de CI. En multirepo, los pipelines son totalmente independientes y sencillos por proyecto, pero comprobar cómo afecta un cambio a tres repositorios distintos exige orquestar CI en cadena, lo que es más costoso y puede dar lugar a romper integraciones.
- Repositorio único abierto frente a acceso restringido por proyecto. El monorepo fomenta la transparencia interna y facilita refactorizaciones globales en una sola transacción, aunque exige mecanismos extra para el control. El multirepo tiende a crear mayor desconexión de conocimiento entre equipos.
- Orquestación avanzada frente a sobrecarga administrativa de múltiples repos. Mantener un monorepo a gran escala requiere herramientas avanzadas que soporten caché remota y computación distribuida. Esto se puede conseguir con herramientas como las que vamos a nombrar más adelante. En multirepo la complejidad no está en la herramienta de build, sino en la gestión administrativa por la necesidad de estandarizar herramientas, versiones o pipelines en decenas de repositorios.
- Criterios de elección según el tamaño del equipo y la arquitectura del sistema. Los monorepos son especialmente efectivos en arquitecturas con un alto grado de código compartido (como aplicaciones que trabajan con distintos stacks por separación de responsabilidades o microfrontends) y en organizaciones con equipos alineados en herramientas comunes. Los multirepos son idóneos para microservicios con ciclos de vida totalmente desacoplados, con equipos autónomos e infraestructuras independientes.
Herramientas y orquestadores principales para la gestión de monorepos
A continuación vamos a ver algunas herramientas populares que nos permitirán trabajar con mono-repositorios mejorando los flujos de trabajo necesarios para la aplicación de esta arquitectura.
Nx para análisis inteligente de grafos de proyectos y escalabilidad de nivel empresarial
Comenzamos nuestra recopilación de herramientas con un clásico: Nx, que destaca por su capacidad para construir un grafo de dependencias interno en tiempo real que mapea las relaciones entre todas las aplicaciones y librerías del repositorio.
Tienes comandos como nx affected que hacen la magia de analizar los archivos modificados en una rama y ejecutar tareas como pruebas, linting o compilación únicamente sobre los proyectos impactados directa o indirectamente.
Turborepo para velocidad y caching de alto rendimiento optimizado para el ecosistema JavaScript/TypeScript
Turborepo es otra de las herramientas más populares del momento y que está adquiriendo bastante tracción después de ser adquirido por Vercel y reescrito en Rust. Esta herramienta prioriza la simplicidad de configuración y la velocidad de ejecución de los flujos de trabajo.
Su modelo mental se basa en definir pipelines de tareas en un archivo turbo.json, donde se establecen las entradas, salidas y dependencias entre scripts, tales como los típicos build o test. Su principal fuerte es el sistema de caché local y remoto que reduce drásticamente los tiempos de compilación.
Bun Workspaces para una ejecución ultrarrápida y gestión nativa de paquetes
Una herramienta un poco más amplia pero que también merece la pena señalar es Bun, un runtime de Javascript bastante novedoso que incluye gestor de paquetes y otras herramientas integradas. Bun ofrece soporte nativo para monorepos a través de workspaces sin requerir capas de adicionales. Además, algunas operativas que en Node se hacen un poco pesadas gracias a Bun corren como la luz, por lo que puedes ejecutar los pipelines mucho más rápido.
Lerna para la gestión de paquetes y publicación automatizada
En el mundo de los monorepos no podemos dejar de mencionar a Lerna, una de las herramientas más antiguas del ecosistema, que está enfocada primordialmente en el ciclo de vida de publicación de librerías. Lerna tuvo unos problemas hace años porque su equipo de desarrollo decidió abandonarlo. Luego la comunidad dio continuidad al proyecto y sigue siendo muy útil para gestionar mono-repositorios de librerías que dependen entre sí, en el ecosistema Node.
Lo que te permite Lerna es la automatización del control de versiones y la publicación masiva en npm, aunque todo el trabajo de separación y mantenimiento de las dependencias lo hace con workspaces de npm nativos.
pnpm y Yarn Workspaces como soluciones nativas de gestión de enlaces simbólicos y aislamiento de dependencias
También podemos utilizar simplemente herramientas conocidas para la gestión de dependencias como pnpm y Yarn. Estas herramientas de hecho son la capa base de infraestructura sobre la cual operan orquestadores como Turborepo o Nx.
Implementación de monorepo con frameworks web modernos
Ya para acabar vamos a ver a ahora algunas recomendaciones, stacks y guías que nos servirán para implementar la arquitectura de monorepos en frameworks web modernos. Como verás, existen tecnologías muy diversas donde les podemos sacar partido.
Sincronización de esquemas de datos y tipos TypeScript entre el backend y los frameworks frontend
Si utilizamos TypeScript sabemos de las ventajas de la gestión de tipos. En monorepos podemos obtener una ventaja importante como compartir tipos entre capas. Al ubicar las entidades de base de datos o esquemas de validación en un paquete interno como @repo/types, tanto el servidor API como los clientes web pueden importar los mismos contratos de datos y mantenerlos sincronizados se hace mucho más fácil.
Configuración de arquitecturas Micro-frontends integrando múltiples aplicaciones en un único repositorio
En la práctica con un mono-repositorio podemos albergar cualquier tipo de proyecto, pero uno de los ámbitos donde le podemos sacar bastante partido es cuando trabajamos con micro-frontends. En estos casos se resuelve el problema de la fragmentación de repositorios, manteniendo la independencia modular.
Reutilización de estilos con Tailwind CSS y Web Components
Con diversos proyectos, si hemos asumido una arquitectura de mono-repositorios, podemos también sacarle mucho partido a la hora de gestionar librerías de interfaz gráfica o esquemas de diseño, ya que nos permite definir de una manera centralizada todo lo que respecta al aspecto, que es asumible por las distintas capas de producto.
Esto nos sirve para preservar la identidad visual de manera coherente, sin duplicar código. Lo puedes conseguir con cualquier librería de diseño, por ejemplo aplicando un mismo tema de Tailwind CSS, o cuando la arquitectura requiere interoperabilidad entre diferentes frameworks gracias a Web Components.
Desarrollo de proyectos full-stack unificados combinando SvelteKit y NestJS
El ejemplo más básico de monorepo se da cuando tienes un mismo proyecto tiene la capa backend y frontend unida en un mismo repositorio. Aquí cualquier tándem de tecnologías es válido, por ejemplo el backend en NestJS y el frontend en SvelteKit, aunque ejemplos hay para aburrir.
Estrategias de enrutamiento y despliegue independiente para sitios multimarca con Remix y Qwik
Poder gestionar de manera exitosa los mono-repositorios puede traer complicaciones. Sin embargo existen herramientas adicionales que puedes integrar como Remix y Qwik. Te recomendamos estudiarlas si llega el caso para mejorar el enrutamiento y despliegue independiente para sitios multimarca.