Retour au blog

Monorepos en JavaScript : Guide Complet avec Turborepo et Nx en 2025

Salut HaWkers, les monorepos sont devenus le choix standard pour les équipes qui gèrent plusieurs packages ou applications connexes. Des entreprises comme Google, Meta, Microsoft et Vercel utilisent des monorepos à grande échelle.

Avez-vous déjà eu à maintenir plusieurs dépôts synchronisés, avec différentes versions de dépendances et des processus de release manuels ? Les monorepos résolvent ces problèmes de manière élégante.

Qu'est-ce qu'un Monorepo et Pourquoi l'Utiliser

Un monorepo est un dépôt unique qui contient plusieurs projets distincts, mais liés. Ce n'est pas la même chose qu'un "monolithe" - chaque projet peut être indépendant.

Avantages des Monorepos

Partage de code simplifié :

  • Les packages internes sont importés directement
  • Les changements se propagent instantanément
  • Pas besoin de publier sur npm pour une utilisation interne

Commits atomiques :

  • Changements affectant plusieurs packages en un seul commit
  • Refactorisations coordonnées
  • Historique des changements unifié

Outils unifiés :

  • Une seule configuration ESLint, Prettier, TypeScript
  • CI/CD centralisé
  • Dépendances dédupliquées

Inconvénients à considérer :

  • Courbe d'apprentissage initiale
  • Le dépôt peut devenir volumineux
  • Nécessite des outils spécialisés pour scaler

Structure Basique d'un Monorepo

Structure de Répertoires

my-monorepo/
├── apps/                    # Applications
│   ├── web/                 # App Next.js
│   │   ├── src/
│   │   ├── package.json
│   │   └── tsconfig.json
│   ├── mobile/              # App React Native
│   │   ├── src/
│   │   └── package.json
│   └── api/                 # API Node.js
│       ├── src/
│       └── package.json

├── packages/                # Packages partagés
│   ├── ui/                  # Composants UI
│   │   ├── src/
│   │   └── package.json
│   ├── utils/               # Fonctions utilitaires
│   │   ├── src/
│   │   └── package.json
│   ├── config-eslint/       # Configuration ESLint
│   │   └── package.json
│   └── config-typescript/   # Configuration TypeScript
│       └── package.json

├── package.json             # Root package.json
├── pnpm-workspace.yaml      # Config workspace (pnpm)
├── turbo.json               # Config Turborepo
└── tsconfig.json            # Config TypeScript de base

Configuration du Workspace (pnpm)

# pnpm-workspace.yaml
packages:
  - 'apps/*'
  - 'packages/*'
// package.json (root)
{
  "name": "my-monorepo",
  "private": true,
  "scripts": {
    "build": "turbo run build",
    "dev": "turbo run dev",
    "lint": "turbo run lint",
    "test": "turbo run test",
    "clean": "turbo run clean && rm -rf node_modules"
  },
  "devDependencies": {
    "turbo": "^2.0.0"
  },
  "packageManager": "pnpm@8.15.0"
}

Configurer Turborepo

Turborepo est l'outil le plus populaire pour gérer des monorepos JavaScript en 2025, acquis par Vercel.

Configuration Basique

// turbo.json
{
  "$schema": "https://turbo.build/schema.json",
  "globalDependencies": ["**/.env.*local"],
  "pipeline": {
    "build": {
      "dependsOn": ["^build"],
      "outputs": ["dist/**", ".next/**", "!.next/cache/**"]
    },
    "lint": {
      "dependsOn": ["^build"]
    },
    "test": {
      "dependsOn": ["build"],
      "inputs": ["src/**/*.tsx", "src/**/*.ts", "test/**/*.ts"]
    },
    "dev": {
      "cache": false,
      "persistent": true
    },
    "clean": {
      "cache": false
    }
  }
}

Pipeline avec Cache Distant

// turbo.json - Configuration avancée
{
  "$schema": "https://turbo.build/schema.json",
  "globalDependencies": [
    ".env",
    "**/.env.*local"
  ],
  "globalEnv": [
    "NODE_ENV",
    "VERCEL_URL"
  ],
  "pipeline": {
    "build": {
      "dependsOn": ["^build"],
      "outputs": ["dist/**", ".next/**", "!.next/cache/**"],
      "env": ["DATABASE_URL", "API_KEY"]
    },
    "build:docker": {
      "dependsOn": ["build"],
      "outputs": ["Dockerfile.built"]
    },
    "test": {
      "dependsOn": ["build"],
      "outputs": ["coverage/**"],
      "inputs": [
        "src/**/*.tsx",
        "src/**/*.ts",
        "**/*.test.ts",
        "**/*.test.tsx"
      ]
    },
    "test:e2e": {
      "dependsOn": ["build"],
      "outputs": ["playwright-report/**"],
      "inputs": ["e2e/**/*.ts"]
    },
    "lint": {
      "outputs": [],
      "inputs": ["**/*.ts", "**/*.tsx", "**/*.js", "**/*.jsx"]
    },
    "typecheck": {
      "dependsOn": ["^build"],
      "outputs": []
    },
    "dev": {
      "cache": false,
      "persistent": true
    }
  }
}

Packages Partagés

Package de Composants UI

// packages/ui/package.json
{
  "name": "@myorg/ui",
  "version": "0.0.0",
  "private": true,
  "main": "./dist/index.js",
  "module": "./dist/index.mjs",
  "types": "./dist/index.d.ts",
  "exports": {
    ".": {
      "types": "./dist/index.d.ts",
      "import": "./dist/index.mjs",
      "require": "./dist/index.js"
    },
    "./button": {
      "types": "./dist/button.d.ts",
      "import": "./dist/button.mjs",
      "require": "./dist/button.js"
    }
  },
  "scripts": {
    "build": "tsup src/index.ts --format cjs,esm --dts",
    "dev": "tsup src/index.ts --format cjs,esm --dts --watch",
    "lint": "eslint src/",
    "clean": "rm -rf dist"
  },
  "devDependencies": {
    "@myorg/config-typescript": "workspace:*",
    "tsup": "^8.0.0",
    "typescript": "^5.3.0"
  },
  "peerDependencies": {
    "react": "^18.0.0"
  }
}
// packages/ui/src/index.ts
export { Button, type ButtonProps } from './button';
export { Input, type InputProps } from './input';
export { Card, type CardProps } from './card';
export { Modal, type ModalProps } from './modal';
// packages/ui/src/button.tsx
import { forwardRef, ButtonHTMLAttributes } from 'react';

export interface ButtonProps extends ButtonHTMLAttributes<HTMLButtonElement> {
  variant?: 'primary' | 'secondary' | 'outline' | 'ghost';
  size?: 'sm' | 'md' | 'lg';
  isLoading?: boolean;
}

export const Button = forwardRef<HTMLButtonElement, ButtonProps>(
  ({ variant = 'primary', size = 'md', isLoading, children, className, disabled, ...props }, ref) => {
    const baseStyles = 'inline-flex items-center justify-center font-medium transition-colors focus:outline-none focus:ring-2 focus:ring-offset-2 disabled:opacity-50 disabled:cursor-not-allowed';

    const variants = {
      primary: 'bg-blue-600 text-white hover:bg-blue-700 focus:ring-blue-500',
      secondary: 'bg-gray-600 text-white hover:bg-gray-700 focus:ring-gray-500',
      outline: 'border-2 border-gray-300 bg-transparent hover:bg-gray-100 focus:ring-gray-500',
      ghost: 'bg-transparent hover:bg-gray-100 focus:ring-gray-500',
    };

    const sizes = {
      sm: 'px-3 py-1.5 text-sm rounded',
      md: 'px-4 py-2 text-base rounded-md',
      lg: 'px-6 py-3 text-lg rounded-lg',
    };

    return (
      <button
        ref={ref}
        className={`${baseStyles} ${variants[variant]} ${sizes[size]} ${className || ''}`}
        disabled={disabled || isLoading}
        {...props}
      >
        {isLoading ? (
          <>
            <svg className="animate-spin -ml-1 mr-2 h-4 w-4" fill="none" viewBox="0 0 24 24">
              <circle className="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="4" />
              <path className="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4z" />
            </svg>
            Chargement...
          </>
        ) : (
          children
        )}
      </button>
    );
  }
);

Button.displayName = 'Button';

Package Utilitaires

// packages/utils/src/index.ts
export * from './string';
export * from './date';
export * from './validation';
export * from './formatting';
// packages/utils/src/validation.ts
export const validators = {
  email: (value: string): boolean => {
    const regex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
    return regex.test(value);
  },

  siret: (value: string): boolean => {
    const siret = value.replace(/\D/g, '');
    if (siret.length !== 14) return false;

    let sum = 0;
    for (let i = 0; i < 14; i++) {
      let digit = parseInt(siret.charAt(i));
      if (i % 2 === 0) {
        digit *= 2;
        if (digit > 9) digit -= 9;
      }
      sum += digit;
    }

    return sum % 10 === 0;
  },

  phone: (value: string): boolean => {
    const phone = value.replace(/\D/g, '');
    return phone.length >= 10 && phone.length <= 11;
  },

  url: (value: string): boolean => {
    try {
      new URL(value);
      return true;
    } catch {
      return false;
    }
  },
};

export function createValidator<T extends Record<string, unknown>>(
  rules: Record<keyof T, (value: unknown) => string | null>
) {
  return (data: T): Record<keyof T, string | null> => {
    const errors = {} as Record<keyof T, string | null>;

    for (const [field, validator] of Object.entries(rules)) {
      errors[field as keyof T] = validator(data[field as keyof T]);
    }

    return errors;
  };
}

Utiliser Nx comme Alternative

Nx est plus opiniâtre que Turborepo et offre plus d'outils built-in.

Setup de Nx

npx create-nx-workspace@latest my-workspace --preset=ts
// nx.json
{
  "$schema": "./node_modules/nx/schemas/nx-schema.json",
  "targetDefaults": {
    "build": {
      "dependsOn": ["^build"],
      "inputs": ["production", "^production"],
      "cache": true
    },
    "test": {
      "inputs": ["default", "^production"],
      "cache": true
    },
    "lint": {
      "inputs": ["default", "{workspaceRoot}/.eslintrc.json"],
      "cache": true
    }
  },
  "namedInputs": {
    "default": ["{projectRoot}/**/*", "sharedGlobals"],
    "production": [
      "default",
      "!{projectRoot}/**/*.spec.ts",
      "!{projectRoot}/**/*.test.ts"
    ],
    "sharedGlobals": ["{workspaceRoot}/tsconfig.base.json"]
  },
  "plugins": [
    "@nx/vite/plugin",
    "@nx/eslint/plugin"
  ]
}

Générateurs Nx

# Créer une nouvelle bibliothèque
npx nx generate @nx/js:library my-lib --directory=packages/my-lib

# Créer une nouvelle app React
npx nx generate @nx/react:application my-app --directory=apps/my-app

# Visualiser les dépendances
npx nx graph

Configurations Partagées

ESLint Partagé

// packages/config-eslint/index.js
module.exports = {
  extends: [
    'eslint:recommended',
    'plugin:@typescript-eslint/recommended',
    'plugin:react/recommended',
    'plugin:react-hooks/recommended',
    'prettier',
  ],
  parser: '@typescript-eslint/parser',
  plugins: ['@typescript-eslint', 'react', 'react-hooks'],
  parserOptions: {
    ecmaVersion: 'latest',
    sourceType: 'module',
    ecmaFeatures: {
      jsx: true,
    },
  },
  settings: {
    react: {
      version: 'detect',
    },
  },
  rules: {
    'react/react-in-jsx-scope': 'off',
    'react/prop-types': 'off',
    '@typescript-eslint/no-unused-vars': ['error', { argsIgnorePattern: '^_' }],
    '@typescript-eslint/explicit-function-return-type': 'off',
    '@typescript-eslint/no-explicit-any': 'warn',
  },
  ignorePatterns: ['dist', 'node_modules', '.turbo'],
};
// apps/web/.eslintrc.js
module.exports = {
  root: true,
  extends: ['@myorg/config-eslint'],
  parserOptions: {
    project: './tsconfig.json',
    tsconfigRootDir: __dirname,
  },
};

TypeScript Partagé

// packages/config-typescript/base.json
{
  "$schema": "https://json.schemastore.org/tsconfig",
  "compilerOptions": {
    "strict": true,
    "esModuleInterop": true,
    "skipLibCheck": true,
    "forceConsistentCasingInFileNames": true,
    "moduleResolution": "bundler",
    "resolveJsonModule": true,
    "isolatedModules": true,
    "noEmit": true,
    "declaration": true,
    "declarationMap": true,
    "inlineSources": false,
    "preserveWatchOutput": true
  },
  "exclude": ["node_modules"]
}
// packages/config-typescript/react.json
{
  "$schema": "https://json.schemastore.org/tsconfig",
  "extends": "./base.json",
  "compilerOptions": {
    "lib": ["DOM", "DOM.Iterable", "ES2022"],
    "jsx": "react-jsx",
    "module": "ESNext",
    "target": "ES2022"
  }
}

CI/CD pour Monorepos

GitHub Actions Optimisé

# .github/workflows/ci.yml
name: CI

on:
  push:
    branches: [main]
  pull_request:
    branches: [main]

jobs:
  build:
    runs-on: ubuntu-latest

    steps:
      - uses: actions/checkout@v4
        with:
          fetch-depth: 2

      - uses: pnpm/action-setup@v2
        with:
          version: 8

      - uses: actions/setup-node@v4
        with:
          node-version: 20
          cache: 'pnpm'

      - name: Install dependencies
        run: pnpm install --frozen-lockfile

      - name: Build affected packages
        run: pnpm turbo run build --filter=...[HEAD^1]

      - name: Test affected packages
        run: pnpm turbo run test --filter=...[HEAD^1]

      - name: Lint affected packages
        run: pnpm turbo run lint --filter=...[HEAD^1]

  deploy:
    needs: build
    if: github.ref == 'refs/heads/main'
    runs-on: ubuntu-latest

    steps:
      - uses: actions/checkout@v4

      - uses: pnpm/action-setup@v2
        with:
          version: 8

      - uses: actions/setup-node@v4
        with:
          node-version: 20
          cache: 'pnpm'

      - run: pnpm install --frozen-lockfile

      - name: Build all
        run: pnpm turbo run build

      - name: Deploy web
        run: pnpm --filter=@myorg/web deploy
        env:
          VERCEL_TOKEN: ${{ secrets.VERCEL_TOKEN }}

Conclusion

Les monorepos offrent des avantages significatifs pour les équipes qui gèrent plusieurs packages liés. Les outils modernes comme Turborepo et Nx ont rendu la gestion des monorepos accessible même pour les petites équipes.

Quand utiliser un monorepo :

  • Plusieurs packages avec du code partagé
  • Équipes travaillant sur des features transversales
  • Besoin de releases coordonnées

Quand éviter :

  • Projets complètement indépendants
  • Équipes avec peu de communication
  • Dépôt unique très petit

Si vous voulez approfondir vos connaissances en architecture JavaScript, je vous recommande de jeter un œil à un autre article : Vite vs Webpack en 2025 où vous découvrirez comment choisir l'outil de build idéal pour vos packages.

C'est parti ! 🦅

🎯 Rejoignez les Développeurs qui Évoluent

Des milliers de développeurs utilisent déjà notre matériel pour accélérer leurs études et obtenir de meilleures positions sur le marché.

Pourquoi investir dans une connaissance structurée ?

Apprendre de manière organisée et avec des exemples pratiques fait toute la différence dans votre parcours de développeur.

Commencez maintenant :

  • €9,90 (paiement unique)

🚀 Accéder au Guide Complet

"Excellent matériel pour ceux qui veulent approfondir !" - Jean, Développeur

Commentaires (0)

Cet article n'a pas encore de commentaires. Soyez le premier!

Ajouter des commentaires