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 install and 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.

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.yml for local dev
  • CI cache that avoids rebuilding what hasn’t changed

What needs attention:

  • composer install and bun install are 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.

KD

Kevin De Vaubree

Senior Full-Stack Developer