Compartir código entre aplicaciones usando Yarn Symlinks: Una alternativa simple a los monorepos complejos
8 min de lectura

Compartir código entre aplicaciones usando Yarn Symlinks: Una alternativa simple a los monorepos complejos

1650 palabras

¿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:

  1. 📁 Copiar y pegar el código en cada proyecto
  2. 📦 Publicar paquetes npm separados
  3. 🏗️ 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

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.

✅ 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

  1. Simplicidad: Configuración mínima comparado con monorepos complejos
  2. Desarrollo en tiempo real: Cambios instantáneos entre proyectos
  3. Sin duplicación: Un solo lugar para el código compartido
  4. Flexibilidad: Cada proyecto mantiene su independencia
  5. Sin overhead: No necesitas herramientas adicionales complejas

❌ Desventajas

  1. Versionado limitado: No puedes bloquear versiones específicas
  2. Frágil: Cambios en shared-lib pueden romper todas las apps
  3. CI/CD más complejo: Necesitas construir shared-lib primero
  4. Limitaciones con algunas herramientas: Como Create React App
  5. 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:

Comentarios

Últimas Entradas

8 min

1627 palabras

Have you ever found yourself duplicating utilities, data types, or components across different applications? If you work on multiple projects that need to share common code, you’ve likely faced the dilemma of how to manage this shared code efficiently.

Recently, while exploring different approaches to share code between applications, I stumbled upon an elegant and simple solution that many developers overlook: Yarn symlinks. This technique might be the perfect answer if you’re looking for a lightweight alternative to complex monorepo setups.

7 min

1279 palabras

¿Estás cansado de ver imports como import Logger from "../../../utils/logger" en tus proyectos de Node.js? Si desarrollas aplicaciones con estructuras de carpetas complejas, seguramente te has encontrado con el laberinto de puntos y barras que pueden llegar a ser los imports relativos. Afortunadamente, TypeScript ofrece una solución elegante: los Path Aliases.

En esta guía completa aprenderás a configurar path aliases en proyectos Node.js con TypeScript, eliminando para siempre esos imports confusos y mejorando significativamente la legibilidad y mantenibilidad de tu código.

6 min

1231 palabras

Are you tired of seeing imports like import Logger from "../../../utils/logger" in your Node.js projects? If you develop applications with complex folder structures, you’ve surely encountered the labyrinth of dots and slashes that relative imports can become. Fortunately, TypeScript offers an elegant solution: Path Aliases.

In this complete guide, you’ll learn to configure path aliases in Node.js projects with TypeScript, forever eliminating those confusing imports and significantly improving the readability and maintainability of your code.

10 min

1964 palabras

CookieStore API: El Futuro Asíncrono de la Gestión de Cookies en JavaScript

Durante décadas, los desarrolladores web hemos dependido de la antigua y limitada interfaz document.cookie para manejar cookies en el navegador. Esta API síncrona, con su sintaxis peculiar de cadenas de texto, ha sido fuente de frustración y errores. Pero eso está cambiando con la llegada de CookieStore API, una interfaz moderna y asíncrona que promete revolucionar cómo interactuamos with cookies.

7 min

1379 palabras

CookieStore API: The Async Future of Cookie Management in JavaScript

For decades, web developers have depended on the old and limited document.cookie interface to handle cookies in the browser. This synchronous API, with its peculiar string syntax, has been a source of frustration and errors. But that’s changing with the arrival of CookieStore API, a modern and asynchronous interface that promises to revolutionize how we interact with cookies.

The Problem with document.cookie

Before diving into CookieStore, let’s recall the headaches document.cookie has caused us:

14 min

2873 palabras

La Filament v4 Beta ha llegado oficialmente, y es sin duda la actualización más ambiciosa y completa en la historia de este framework. Después de explorar en detalle todas las nuevas características, puedo afirmar que esta versión representa un salto cuántico en términos de rendimiento, facilidad de uso y capacidades de desarrollo.

En este análisis exhaustivo, vamos a explorar cada una de las nuevas características de Filament v4, explicando no solo qué es nuevo, sino también cómo estas mejoras pueden transformar tu flujo de trabajo y las posibilidades de tus aplicaciones.