¿Te has encontrado alguna vez duplicando utilidades, tipos de datos o componentes entre diferentes aplicaciones? Si trabajas en múltiples proyectos que necesitan compartir código común, seguramente has enfrentado el dilema de cómo gestionar este código compartido de manera eficiente.
Recientemente, mientras exploraba diferentes enfoques para compartir código entre aplicaciones, me topé con una solución elegante y simple que muchos desarrolladores pasan por alto: los symlinks de Yarn. Esta técnica puede ser la respuesta perfecta si buscas una alternativa liviana a las configuraciones complejas de monorepos.
El Problema: Código Duplicado Everywhere
Imagina que tienes tres aplicaciones: un frontend React, una API Node.js y una aplicación móvil React Native. Todas necesitan:
- Validadores comunes
- Tipos TypeScript compartidos
- Utilidades para formatear fechas
- Funciones de validación de email
- Constantes de la aplicación
El enfoque tradicional sería:
- 📁 Copiar y pegar el código en cada proyecto
- 📦 Publicar paquetes npm separados
- 🏗️ Configurar un monorepo complejo con Lerna o Rush
Los problemas son evidentes:
- 🔴 Duplicación: El mismo código en múltiples lugares
- 🔴 Inconsistencias: Versiones diferentes del mismo código
- 🔴 Mantenimiento: Actualizar en múltiples lugares
- 🔴 Complejidad: Configuraciones pesadas para problemas simples
La Solución: Yarn Symlinks
Los symlinks de Yarn ofrecen una solución intermedia elegante. Con un simple comando, puedes enlazar código local entre proyectos:
yarn add link:/ruta/a/la/carpeta/local
Este comando crea un enlace simbólico a un paquete que está en tu sistema de archivos local, perfecto para desarrollar paquetes relacionados en entornos de monorepo.
¿Cuándo Usar Yarn Symlinks?
✅ Es Ideal Si:
- Buscas una solución simple para compartir código
- Usualmente liberas tus aplicaciones juntas (ej: frontend + backend)
- Quieres desarrollo en tiempo real sin builds complejos
- Tienes un equipo pequeño trabajando en proyectos relacionados
❌ No Es La Mejor Opción Si:
- Quieres publicar paquetes independientemente
- Necesitas bloquear versiones específicas del código compartido
- Trabajas con equipos distribuidos con ciclos de release diferentes
- Requieres versionado semántico estricto
Configuración Paso a Paso
Estructura del Proyecto
Vamos a crear un monorepo simple con dos aplicaciones y una biblioteca compartida:
monorepo-ejemplo/
├── shared-lib/
│ ├── package.json
│ ├── yarn.lock
│ └── src/
│ ├── index.ts
│ ├── utils/
│ └── types/
├── app-frontend/
│ ├── package.json
│ ├── yarn.lock
│ └── src/
├── app-backend/
│ ├── package.json
│ ├── yarn.lock
│ └── src/
└── README.md
Paso 1: Crear la Biblioteca Compartida
mkdir monorepo-ejemplo && cd monorepo-ejemplo
mkdir shared-lib && cd shared-lib
npm init -y
Crear la estructura básica:
// shared-lib/src/index.ts
export * from './utils/validators';
export * from './utils/formatters';
export * from './types/common';
// shared-lib/src/utils/validators.ts
export const isValidEmail = (email: string): boolean => {
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
return emailRegex.test(email);
};
export const isValidPhone = (phone: string): boolean => {
const phoneRegex = /^\+?[\d\s\-\(\)]+$/;
return phoneRegex.test(phone);
};
// shared-lib/src/utils/formatters.ts
export const formatCurrency = (amount: number, currency = 'EUR'): string => {
return new Intl.NumberFormat('es-ES', {
style: 'currency',
currency,
}).format(amount);
};
export const formatDate = (date: Date): string => {
return new Intl.DateTimeFormat('es-ES').format(date);
};
// shared-lib/src/types/common.ts
export interface User {
id: string;
email: string;
name: string;
createdAt: Date;
}
export interface ApiResponse<T> {
success: boolean;
data?: T;
error?: string;
}
Configurar el package.json
:
{
"name": "shared-lib",
"version": "1.0.0",
"main": "dist/index.js",
"types": "dist/index.d.ts",
"scripts": {
"build": "tsc",
"dev": "tsc --watch"
},
"devDependencies": {
"typescript": "^5.0.0"
}
}
Paso 2: Configurar TypeScript
// shared-lib/tsconfig.json
{
"compilerOptions": {
"target": "ES2020",
"module": "commonjs",
"outDir": "./dist",
"rootDir": "./src",
"declaration": true,
"strict": true,
"esModuleInterop": true,
"skipLibCheck": true
},
"include": ["src/**/*"],
"exclude": ["node_modules", "dist"]
}
Paso 3: Crear las Aplicaciones
cd ..
mkdir app-frontend && cd app-frontend
npm init -y
cd ..
mkdir app-backend && cd app-backend
npm init -y
Paso 4: Enlazar la Biblioteca Compartida
Desde cada aplicación, ejecuta:
# En app-frontend/
cd app-frontend
yarn add link:../shared-lib
# En app-backend/
cd ../app-backend
yarn add link:../shared-lib
Esto agregará shared-lib
como dependencia en ambos package.json
:
{
"dependencies": {
"shared-lib": "link:../shared-lib"
}
}
Paso 5: Usar el Código Compartido
// app-frontend/src/components/UserForm.tsx
import React, { useState } from 'react';
import { isValidEmail, User } from 'shared-lib';
export const UserForm: React.FC = () => {
const [email, setEmail] = useState('');
const [isValid, setIsValid] = useState(false);
const handleEmailChange = (value: string) => {
setEmail(value);
setIsValid(isValidEmail(value));
};
return (
<form>
<input
type="email"
value={email}
onChange={(e) => handleEmailChange(e.target.value)}
style={{ borderColor: isValid ? 'green' : 'red' }}
/>
<p>Email {isValid ? 'válido' : 'inválido'}</p>
</form>
);
};
// app-backend/src/controllers/userController.ts
import { isValidEmail, User, ApiResponse } from 'shared-lib';
export class UserController {
async createUser(userData: any): Promise<ApiResponse<User>> {
if (!isValidEmail(userData.email)) {
return {
success: false,
error: 'Email inválido'
};
}
// Lógica de creación de usuario...
const user: User = {
id: generateId(),
email: userData.email,
name: userData.name,
createdAt: new Date()
};
return {
success: true,
data: user
};
}
}
Flujo de Desarrollo Local
Desarrollo en Tiempo Real
Para que los cambios en shared-lib
se reflejen automáticamente:
# Terminal 1: Compilar la biblioteca en modo watch
cd shared-lib
yarn build --watch
# Terminal 2: Desarrollar app-frontend
cd app-frontend
yarn dev
# Terminal 3: Desarrollar app-backend
cd app-backend
yarn dev
Limitaciones con Create React App
Si usas Create React App, necesitarás algunas configuraciones adicionales porque CRA no detecta cambios fuera de su carpeta src
:
# Instalar CRACO para personalizar webpack
npm install @craco/craco --save-dev
// craco.config.js
const path = require('path');
module.exports = {
webpack: {
configure: (webpackConfig) => {
// Permitir imports desde fuera de src/
webpackConfig.resolve.plugins = webpackConfig.resolve.plugins.filter(
plugin => plugin.constructor.name !== 'ModuleScopePlugin'
);
// Añadir shared-lib a los directorios observados
webpackConfig.watchOptions = {
ignored: /node_modules\/(?!shared-lib)/,
};
return webpackConfig;
},
},
};
Integración Continua
En CI/CD, necesitas construir la biblioteca compartida antes que las aplicaciones:
# .github/workflows/deploy.yml
name: Build and Deploy
on: [push]
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- name: Setup Node.js
uses: actions/setup-node@v2
with:
node-version: '18'
# 1. Construir shared-lib primero
- name: Build shared library
working-directory: ./shared-lib
run: |
yarn install
yarn build
# 2. Construir frontend
- name: Build frontend
working-directory: ./app-frontend
run: |
yarn install # Automáticamente enlaza shared-lib
yarn build
# 3. Construir backend
- name: Build backend
working-directory: ./app-backend
run: |
yarn install
yarn build
Script de Automatización
Para simplificar el desarrollo, puedes crear scripts que gestionen todo:
// package.json (raíz del monorepo)
{
"name": "monorepo-ejemplo",
"scripts": {
"install:all": "yarn install:shared && yarn install:frontend && yarn install:backend",
"install:shared": "cd shared-lib && yarn install",
"install:frontend": "cd app-frontend && yarn install",
"install:backend": "cd app-backend && yarn install",
"build:all": "yarn build:shared && yarn build:frontend && yarn build:backend",
"build:shared": "cd shared-lib && yarn build",
"build:frontend": "cd app-frontend && yarn build",
"build:backend": "cd app-backend && yarn build",
"dev:shared": "cd shared-lib && yarn dev",
"dev:frontend": "cd app-frontend && yarn dev",
"dev:backend": "cd app-backend && yarn dev",
"link:all": "cd app-frontend && yarn add link:../shared-lib && cd ../app-backend && yarn add link:../shared-lib"
},
"devDependencies": {
"concurrently": "^7.6.0"
}
}
Para desarrollo paralelo:
{
"scripts": {
"dev:all": "concurrently \"yarn dev:shared\" \"yarn dev:frontend\" \"yarn dev:backend\""
}
}
Ventajas y Desventajas
✅ Ventajas
- Simplicidad: Configuración mínima comparado con monorepos complejos
- Desarrollo en tiempo real: Cambios instantáneos entre proyectos
- Sin duplicación: Un solo lugar para el código compartido
- Flexibilidad: Cada proyecto mantiene su independencia
- Sin overhead: No necesitas herramientas adicionales complejas
❌ Desventajas
- Versionado limitado: No puedes bloquear versiones específicas
- Frágil: Cambios en shared-lib pueden romper todas las apps
- CI/CD más complejo: Necesitas construir shared-lib primero
- Limitaciones con algunas herramientas: Como Create React App
- No escalable: Para equipos grandes puede ser problemático
Cuándo Evolucionar
Hacia Yarn Workspaces
Si notas que tienes muchas dependencias duplicadas:
// package.json (raíz)
{
"private": true,
"workspaces": [
"shared-lib",
"app-frontend",
"app-backend"
]
}
Hacia Lerna
Si necesitas publicar paquetes independientemente:
npm install --global lerna
lerna init
lerna bootstrap
lerna version
lerna publish
Hacia Rush o Nx
Para equipos grandes con necesidades complejas de build y testing.
Casos de Uso Reales
Startup Tech Stack
startup-monorepo/
├── shared-core/ # Tipos, validadores, constantes
├── web-app/ # React frontend
├── mobile-app/ # React Native
├── api-server/ # Node.js API
└── admin-panel/ # Admin React app
Microservicios con Código Compartido
microservices-suite/
├── shared-types/ # TypeScript interfaces
├── shared-utils/ # Utilidades comunes
├── user-service/ # Microservicio de usuarios
├── payment-service/ # Microservicio de pagos
└── notification-service/ # Microservicio de notificaciones
Mejores Prácticas
1. Estructura Clara de la Biblioteca Compartida
// shared-lib/src/index.ts
// Exportaciones organizadas por módulo
export * from './validation';
export * from './formatting';
export * from './types';
export * from './constants';
export * from './utils';
2. Versionado Semántico Manual
Aunque no uses npm publish, mantén un changelog:
# CHANGELOG.md
## [1.2.0] - 2024-07-15
### Added
- Nueva función formatCurrency
- Validador isValidPhone
### Changed
- isValidEmail ahora es más estricto
### Breaking Changes
- Renombrado User.createdDate a User.createdAt
3. Testing del Código Compartido
// shared-lib/package.json
{
"scripts": {
"test": "jest",
"test:watch": "jest --watch"
}
}
4. Documentación Clara
/**
* Valida si un email tiene formato correcto
* @param email - El email a validar
* @returns true si el email es válido
* @example
* ```typescript
* isValidEmail('user@example.com') // true
* isValidEmail('invalid-email') // false
* ```
*/
export const isValidEmail = (email: string): boolean => {
// ...
};
Conclusión
Los Yarn symlinks ofrecen una solución elegante y simple para compartir código entre aplicaciones relacionadas. No es la solución perfecta para todos los casos, pero puede ser ideal si buscas:
- Una alternativa liviana a configuraciones complejas de monorepo
- Desarrollo en tiempo real entre proyectos relacionados
- Simplicidad sobre funcionalidades avanzadas
- Una solución intermedia antes de evolucionar a herramientas más complejas
¿Cuándo considerar esta solución?
- Equipos pequeños (2-5 desarrolladores)
- Proyectos relacionados con ciclos de release similares
- Necesidad de compartir código sin complejidad adicional
- Como paso intermedio hacia un monorepo más sofisticado
La clave está en entender que no existe una solución única para todos los casos. Los Yarn symlinks pueden ser perfectos para tu situación actual, y siempre puedes evolucionar hacia herramientas más complejas cuando tu proyecto lo requiera.
¿Has usado symlinks en tus proyectos? ¿Qué estrategias usas para compartir código entre aplicaciones? ¡Comparte tu experiencia en los comentarios!
Recursos adicionales: