Monorepo avec Turborepo : Organiser un Projet Full-Stack PHP + TypeScript
Le monorepo est revenu en force. Après des années de microservices dans des repos séparés — avec la synchronisation des versions, les CI/CD en cascade et les PRs croisées — de plus en plus d’équipes reviennent à un repository unique. Turborepo rend cette approche viable même pour des projets full-stack mêlant PHP et TypeScript.
Pourquoi un monorepo en 2026
Un seul repo, c’est :
- Un commit atomique pour un changement qui touche le backend et le frontend
- Une seule CI avec du cache intelligent
- Des packages partagés (types, configs, utils) sans publication npm
- Un onboarding simplifié :
git clone+bun installet c’est parti
Les arguments contre :
- Complexité de la CI initiale
- Taille du repo qui grandit
- Droits d’accès plus grossiers (atténué par CODEOWNERS)
Mon verdict après 3 projets en monorepo full-stack : les avantages l’emportent largement dès que l’équipe dépasse 3 développeurs et que le frontend/backend doivent évoluer ensemble.
Ce choix s’inscrit dans une réflexion architecturale plus large. Pour les fondamentaux, voir mon article sur l’architecture logicielle pour applications web modernes.
Structure recommandée
monorepo/
├── apps/
│ ├── api/ # Symfony ou Laravel
│ │ ├── src/
│ │ ├── composer.json
│ │ └── Makefile
│ ├── web/ # Next.js
│ │ ├── src/
│ │ └── package.json
│ └── admin/ # Vue.js (optionnel)
│ ├── src/
│ └── package.json
├── packages/
│ ├── ui/ # Design system partagé
│ │ ├── src/
│ │ └── package.json
│ ├── types/ # Types TypeScript partagés
│ │ ├── src/
│ │ └── package.json
│ ├── config-eslint/ # Config ESLint partagée
│ └── config-typescript/# Config TS partagée
├── turbo.json
├── package.json # Workspace root
└── docker-compose.yml
Les apps/ contiennent les applications déployables. Les packages/ contiennent le code partagé. Turborepo orchestre le tout.
Configurer Turborepo avec PHP + TypeScript
Le défi principal : Turborepo est conçu pour l’écosystème JavaScript. Le backend PHP nécessite une intégration via des scripts npm qui wrappent les commandes PHP.
// apps/api/package.json
{
"name": "@monorepo/api",
"scripts": {
"build": "echo 'PHP build not needed'",
"test": "php bin/phpunit",
"lint": "php vendor/bin/phpstan analyse",
"dev": "symfony server:start --port=8000"
}
}
// turbo.json
{
"$schema": "https://turbo.build/schema.json",
"tasks": {
"build": {
"dependsOn": ["^build"],
"outputs": [".next/**", "dist/**"]
},
"test": {
"dependsOn": ["build"]
},
"lint": {},
"dev": {
"cache": false,
"persistent": true
}
}
}
La commande turbo run test exécute les tests de toutes les apps en parallèle — PHPUnit pour le backend, Vitest pour le frontend. Le cache Turborepo skip les apps qui n’ont pas changé.
Le package types partagé
Le vrai gain du monorepo : partager les types entre frontend et backend.
// packages/types/src/order.ts
export interface Order {
id: string
status: 'pending' | 'confirmed' | 'shipped' | 'delivered'
items: OrderItem[]
total: number
createdAt: string
}
export interface OrderItem {
productId: string
quantity: number
unitPrice: number
}
Le frontend importe directement ces types sans package npm, sans publication, sans version à synchroniser. Quand l’API évolue, le type change dans un seul commit et TypeScript détecte immédiatement les incompatibilités dans le frontend.
CI/CD avec cache Turborepo
# .github/workflows/ci.yml
name: CI
on: [push, pull_request]
jobs:
ci:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: shivammathur/setup-php@v2
with:
php-version: '8.3'
- uses: oven-sh/setup-bun@v1
- name: Install PHP deps
run: cd apps/api && composer install --no-interaction
- name: Install JS deps
run: bun install
- name: Lint + Test + Build
run: bunx turbo run lint test build
Le cache Turborepo réduit drastiquement les temps de CI. Sur un projet avec 4 apps et 6 packages, la CI moyenne est passée de 12 minutes à 3 minutes grâce au cache remote.
Pour les backends Symfony, le pattern CQRS avec Messenger se prête particulièrement bien au monorepo : les commandes et queries sont des contrats clairs qui documentent l’API.
Les frontends Next.js bénéficient aussi du monorepo — les React Server Components peuvent importer directement les packages partagés sans configuration supplémentaire.
Retour d’expérience
Après 18 mois sur un monorepo full-stack (Symfony API + Next.js frontend + admin Vue.js) :
Ce qui marche :
- Commits atomiques cross-stack (changement API + frontend dans une PR)
- Design system partagé via le package
ui/ - Types synchronisés automatiquement
- Un seul
docker-compose.ymlpour le dev local - Cache CI qui évite de rebuilder ce qui n’a pas changé
Ce qui demande de l’attention :
- Le
composer installet lebun installsont deux mondes séparés — pas de résolution de dépendances cross-runtime - Les hooks pre-commit doivent cibler les bons fichiers (pas de PHPStan sur du TypeScript)
- Le CODEOWNERS doit être précis pour ne pas noyer les reviewers
Conseil clé : commencez avec 2 apps maximum (1 backend + 1 frontend) et ajoutez des packages partagés quand le besoin se manifeste. Ne structurez pas pour 10 apps dès le jour 1.
Pour mesurer l’impact du monorepo sur les performances de vos apps, les Core Web Vitals sont un indicateur précieux — le partage de composants optimisés via le design system améliore naturellement les métriques.
En résumé
Le monorepo full-stack avec Turborepo est un game changer pour les équipes qui maintiennent un backend PHP et un frontend JavaScript. Un seul repo, un seul CI, des packages partagés, et un cache intelligent. Le setup initial demande un investissement, mais le ROI se mesure dès la première PR cross-stack.
Kevin De Vaubree
Développeur Full-Stack Senior