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 baseConfiguration 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)
"Excellent matériel pour ceux qui veulent approfondir !" - Jean, Développeur

