Monorepo with Turborepo: Organizing a Full-Stack PHP + TypeScript Project
The monorepo is back in force. After years of microservices in separate repos — with version synchronization, cascading CI/CD, and cross-repo PRs — more teams are returning to a single repository. Turborepo makes this approach viable even for full-stack projects mixing PHP and TypeScript.
Why a monorepo in 2026
A single repo means:
- One atomic commit for a change touching backend and frontend
- A single CI with smart caching
- Shared packages (types, configs, utils) without npm publishing
- Simplified onboarding:
git clone+bun installand you’re off
Arguments against:
- Initial CI complexity
- Growing repo size
- Coarser access rights (mitigated by CODEOWNERS)
My verdict after 3 full-stack monorepo projects: advantages far outweigh once the team exceeds 3 developers and frontend/backend must evolve together.
This choice is part of a broader architectural reflection. For fundamentals, see my article on software architecture for modern web applications.
Recommended structure
monorepo/
├── apps/
│ ├── api/ # Symfony or Laravel
│ │ ├── src/
│ │ ├── composer.json
│ │ └── Makefile
│ ├── web/ # Next.js
│ │ ├── src/
│ │ └── package.json
│ └── admin/ # Vue.js (optional)
│ ├── src/
│ └── package.json
├── packages/
│ ├── ui/ # Shared design system
│ │ ├── src/
│ │ └── package.json
│ ├── types/ # Shared TypeScript types
│ │ ├── src/
│ │ └── package.json
│ ├── config-eslint/ # Shared ESLint config
│ └── config-typescript/# Shared TS config
├── turbo.json
├── package.json # Workspace root
└── docker-compose.yml
apps/ contains deployable applications. packages/ contains shared code. Turborepo orchestrates everything.
Configuring Turborepo with PHP + TypeScript
The main challenge: Turborepo is designed for the JavaScript ecosystem. The PHP backend requires integration via npm scripts that wrap PHP commands.
// 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
}
}
}
The turbo run test command runs tests for all apps in parallel — PHPUnit for backend, Vitest for frontend. Turborepo cache skips apps that haven’t changed.
The shared types package
The real monorepo gain: sharing types between frontend and 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
}
The frontend imports these types directly without npm package, without publishing, without version synchronization. When the API evolves, the type changes in a single commit and TypeScript immediately detects incompatibilities in the frontend.
CI/CD with Turborepo cache
# .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
Turborepo cache drastically reduces CI times. On a project with 4 apps and 6 packages, average CI went from 12 minutes to 3 minutes thanks to remote cache.
For Symfony backends, the CQRS with Messenger pattern fits particularly well in monorepos: commands and queries are clear contracts that document the API.
Next.js frontends also benefit from the monorepo — React Server Components can directly import shared packages without additional configuration.
Feedback
After 18 months on a full-stack monorepo (Symfony API + Next.js frontend + Vue.js admin):
What works:
- Atomic cross-stack commits (API change + frontend in one PR)
- Shared design system via
ui/package - Automatically synchronized types
- Single
docker-compose.ymlfor local dev - CI cache that avoids rebuilding what hasn’t changed
What needs attention:
composer installandbun installare separate worlds — no cross-runtime dependency resolution- Pre-commit hooks must target the right files (no PHPStan on TypeScript)
- CODEOWNERS must be precise to not overwhelm reviewers
Key advice: start with maximum 2 apps (1 backend + 1 frontend) and add shared packages when the need arises. Don’t structure for 10 apps from day 1.
To measure the monorepo’s impact on your apps’ performance, Core Web Vitals are a valuable indicator — sharing optimized components via the design system naturally improves metrics.
In summary
The full-stack monorepo with Turborepo is a game changer for teams maintaining a PHP backend and JavaScript frontend. Single repo, single CI, shared packages, and smart caching. Initial setup requires investment, but ROI is measurable from the first cross-stack PR.
Kevin De Vaubree
Senior Full-Stack Developer