Monorepositorio con JavaScript: organización del código en la práctica
En el desarrollo web moderno con JavaScript (Node.js), cada vez surge más la pregunta: ¿cómo organizar eficientemente el código cuando tienes varios paquetes, bibliotecas o microservicios que dependen entre sí? El enfoque tradicional con repositorios separados (multirepo) puede provocar duplicación de código, dificultades con la sincronización de versiones y un CI/CD lento. La solución es un monorepositorio (monorepo). En este artículo, analizaremos qué es un monorepositorio, para qué sirve y cómo organizarlo en la práctica utilizando JavaScript.
¿Qué es un monorepositorio y para qué sirve?
Un monorepositorio es un enfoque en el que el código de varios proyectos (paquetes, bibliotecas, servicios) se almacena en un único repositorio. No se trata simplemente de "poner todo en una carpeta", sino de una arquitectura bien pensada que resuelve múltiples problemas:
- Versión única de dependencias: evitas situaciones en las que diferentes paquetes usan distintas versiones de la misma biblioteca.
- Refactorización sencilla: los cambios en una biblioteca compartida son visibles de inmediato en todos los proyectos que la utilizan.
- CI/CD simplificado: un repositorio, una configuración de compilación y pruebas.
- Uso compartido de código: utilidades, tipos y configuraciones están disponibles para todos los paquetes sin necesidad de publicarlos en npm.
Sin embargo, el monorepositorio no es adecuado para todos. Requiere disciplina en el equipo y buenas herramientas. Para JavaScript (Node.js), las herramientas principales son npm workspaces (integrado en npm 7+) y Lerna (una herramienta más avanzada).
Configuración de un monorepositorio básico con npm workspaces
Empecemos con un ejemplo sencillo. Supongamos que tenemos un proyecto que consta de dos paquetes: @myapp/core (el núcleo con la lógica de negocio) y @myapp/server (un servidor en Express).
Creemos la estructura de carpetas:
my-monorepo/├── packages/│ ├── core/│ │ ├── src/│ │ │ └── index.js│ │ ├── package.json│ │ └── ...│ └── server/│ ├── src/│ │ └── index.js│ ├── package.json│ └── ...├── package.json└── ...El package.json raíz debe contener el campo workspaces:
{ "name": "my-monorepo", "private": true, "workspaces": [ "packages/*" ], "scripts": { "start": "node packages/server/src/index.js", "test": "npm run test --workspaces" }}Ahora, en packages/core/package.json:
{ "name": "@myapp/core", "version": "1.0.0", "main": "src/index.js"}Y en packages/server/package.json añadimos la dependencia de core:
{ "name": "@myapp/server", "version": "1.0.0", "dependencies": { "@myapp/core": "*", "express": "^4.18.0" }}Después de ejecutar npm install en la raíz, npm creará enlaces simbólicos entre los paquetes. Ahora, en el código del servidor podemos importar core directamente:
// packages/server/src/index.jsconst express = require('express');const { calculate } = require('@myapp/core');
const app = express();app.get('/', (req, res) => { res.send(calculate(2, 3));});app.listen(3000);Este es un ejemplo básico, pero ya muestra la principal ventaja: los cambios en @myapp/core se ven inmediatamente en @myapp/server sin necesidad de publicar en el registro.
Organización avanzada con Lerna
Cuando el número de paquetes supera los 5-10, npm workspaces puede quedarse corto. Aquí es donde entra Lerna, una herramienta que añade potentes capacidades:
- Ejecutar comandos en todos o en paquetes seleccionados.
- Gestión automática de versiones (modo fixed o independent).
- Pu