✨ Strapi MCP is now Generally Available - let your agents manage your Strapi content ✨

Ecosystem13 min read

The Developer's Guide to Managing Multiple Frontends with Turborepo

December 12, 2025Updated on June 14, 2026

Managing separate repositories for your blog, docs, and marketing sites creates friction when they share code. Each update to your authentication helper requires changes across three codebases, package version synchronization, and full CI rebuilds, even for unchanged components.

Turborepo consolidates related applications into one repository with content-aware caching. Update the authentication helper once, and only affected applications rebuild. Build times can drop significantly as more work is restored from cache.

This guide walks through Turborepo v2.x setup, task configuration, and deployment for multiple frontends sharing code, including a typed Strapi SDK consumed by every app in your monorepo.

In Brief:

  • Turborepo v2.x caches build outputs based on file content, rebuilding only what changed since your last run.
  • Monorepos consolidate shared code, dependencies, and deployments for related applications in a single repository.
  • Configure task pipelines with the tasks key in turbo.json, set up remote caching, and deploy only modified applications while maintaining type safety across your stack.
  • Build times drop significantly through parallel execution and intelligent caching as your codebase grows.

What Is Turborepo?

Turborepo is a high-performance build system for JavaScript and TypeScript monorepos, maintained by Vercel. Currently at v2.x (with v2.5 updates), it optimizes build performance through content-aware caching and parallel task execution. The build system hashes source files, dependencies, and configuration to determine what changed. Unchanged tasks restore cached results instead of rebuilding.

Turborepo works best when multiple projects share code and deploy together. If your blog, docs site, and marketing page all pull from the same Strapi backend, a headless CMS, and share UI components, API clients, or TypeScript types, a monorepo lets you update shared code once and see changes propagate across all frontends in a single commit. If your projects evolve independently with minimal shared dependencies, separate repos are fine.

The Monorepo Problem Turborepo Solves

A monorepo keeps all your related projects, applications, shared libraries, and configuration, in one version-controlled repository. A static site, Gatsby docs site, and React marketing page can share UI kits and TypeScript types. Update a button's API, commit once, and all three apps receive the change immediately, with no package publishing and no version bumps.

.
├── apps
│   ├── blog
│   ├── docs
│   └── marketing
├── packages
│   ├── ui
│   ├── types
│   └── api-client
├── package.json
└── turbo.json

The tradeoff is scale. Without tooling like Turborepo, large unified repos suffer slow builds and complex CI/CD. Turborepo eliminates the multi-repo overhead of publishing internal packages to npm, synchronizing versions, and rebuilding identical code in separate pipelines, while keeping builds fast through caching and parallelism.

How Turborepo Works

Turborepo's speed comes from three mechanisms working together: content-aware hashing, dependency graph execution, and parallel processing.

Content-Aware Caching

When you execute a task, Turborepo creates a hash from your source files, package.json dependencies, environment variables, and build configuration. If that hash matches a cached entry, the tool restores previously generated output in milliseconds instead of rebuilding from scratch.

This content-based approach analyzes file contents rather than timestamps, so you never get stale builds. Change a single line in a shared package, and only the modified package plus its direct dependents rebuild. Everything else restores from cache. Configure what gets cached through the outputs field in your turbo.json:

{
  "tasks": {
    "build": {
      "outputs": ["dist/**", ".next/**", "!.next/cache/**"]
    },
    "test": {
      "outputs": ["coverage/**"]
    }
  }
}

Without the outputs key, Turborepo caches task logs only. The glob negation !.next/cache/ excludes Next.js's internal build cache from your cached artifacts, a detail that matters when you're storing outputs in a remote cache.

Dependency-Aware Task Execution

The build system treats every task as a node in a dependency graph you define in turbo.json. Consider a Next.js blog importing components from a shared ui package. Before the blog compiles, the UI library must finish its own build. Encoding that relationship prevents "module not found" errors and unlocks true parallelism across unrelated workspaces.

{
  "tasks": {
    "build": {
      "dependsOn": ["^build"],
      "outputs": ["dist/**"]
    },
    "test": {
      "dependsOn": ["build"]
    }
  }
}

The caret ^build tells Turborepo to run the build task in every dependency workspace first. Without the caret, "build" means the build task in the same package must complete before tests run.

Parallel Processing and Remote Cache

Turborepo runs independent tasks simultaneously based on your dependency graph, eliminating idle CPU time. This parallelism combines with remote caching to benefit your entire team.

Remote caching pushes build artifacts to a shared store. When you finish a build locally, the system uploads the hashed outputs. Your teammate pulls the branch, runs the build, and gets a near-instant cache hit instead of waiting minutes. CI skips unchanged workspaces entirely.

Setup requires authenticating and linking your repo:

npx turbo login
npx turbo link

Both commands require a Vercel account. Remote caching on Vercel is free on all plans, even if you don't host there. For self-hosted options, community implementations like ducktors/turborepo-remote-cache support S3 and other storage backends, and turbo login --manual lets you point at a custom API endpoint.

Setting Up a Turborepo Monorepo

Installing Turborepo

Scaffold a new monorepo with create-turbo, installation docs:

npx create-turbo@latest         # npm
pnpm dlx create-turbo@latest    # pnpm
yarn dlx create-turbo@latest    # yarn
bunx create-turbo@latest        # bun

To add Turborepo to an existing repo, install it as a dev dependency:

npm install turbo --save-dev

The official docs recommend installing both globally and locally. Global turbo defers to the local version if one exists.

Workspace Structure

Your root package.json declares every workspace (for npm and Yarn), keeps the repository private, and centralizes shared tooling. For pnpm, workspaces are declared in pnpm-workspace.yaml instead. The packageManager field is required in Turborepo v2:

{
  "name": "company-monorepo",
  "private": true,
  "packageManager": "pnpm@8.15.0",
  "workspaces": ["apps/*", "packages/*"],
  "scripts": {
    "dev": "turbo run dev",
    "build": "turbo run build"
  },
  "devDependencies": {
    "turbo": "^2",
    "typescript": "^5.3.0"
  }
}
.
├── apps
│   ├── blog
│   ├── docs
│   └── marketing
└── packages
    ├── api-client
    ├── types
    ├── ui
    └── strapi-client

Applications in apps/ consume code from packages/ through workspace dependencies. The build system caches outputs per package and rebuilds only changed code.

Configuring Tasks in turbo.json

The turbo.json file defines how tasks execute across your repository. All v2.x configurations use the tasks key. The v1 pipeline key is deprecated. If you're migrating, run npx @turbo/codemod update to convert automatically.

Defining Build, Lint, and Test Tasks

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

The inputs field narrows which files contribute to the cache hash. Editing a README won't trigger a test re-run when you scope inputs to source and test files. Set cache: false on development servers to preserve hot reloading, and persistent: true to mark them as long-running processes.

Environment Variables in Task Configuration

Turborepo v2 defaults to Strict Environment Mode, which filters the environment variables available to each task. This is a breaking change from v1's Loose Mode. Tasks that silently consumed CI-injected variables will fail until you explicitly declare them.

Four keys control environment variable handling:

{
  "globalEnv": ["NODE_ENV"],
  "globalPassThroughEnv": ["CI", "GITHUB_TOKEN"],
  "tasks": {
    "build": {
      "env": ["DATABASE_URL", "API_BASE_URL"],
      "passThroughEnv": ["SENTRY_AUTH_TOKEN"]
    }
  }
}

Variables in env and globalEnv are included in the cache hash, so changing their values causes a cache miss. Variables in passThroughEnv and globalPassThroughEnv are available at runtime but don't affect caching. This distinction matters: your SENTRY_AUTH_TOKEN for uploading source maps shouldn't bust the build cache, but your DATABASE_URL should.

Turborepo also supports framework inference, so you don't need to list framework-prefixed variables explicitly.

Sharing Code Across Applications

Inside your monorepo, reusable code lives as internal packages. These packages skip the publish-to-npm workflow because workspace tooling handles linking. Your blog, docs, and admin apps all import from the same Strapi headless CMS client without version conflicts.

Creating Internal Packages

packages/strapi-client/
├─ src/
│  ├─ client.ts
│  └─ index.ts
├─ package.json
└─ tsconfig.json

Your package manifest declares a name and explicit export map:

{
  "name": "@repo/strapi-client",
  "version": "0.0.1",
  "private": true,
  "main": "./dist/index.js",
  "types": "./dist/index.d.ts",
  "exports": {
    ".": {
      "import": "./dist/index.js",
      "types": "./dist/index.d.ts"
    }
  }
}

Add the package to any application workspace:

npm add @repo/strapi-client@workspace:^

Then import it directly:

import { createClient } from '@repo/strapi-client';

For TypeScript monorepos, configure path aliases in a shared tsconfig.base.json and extend it in each package. This keeps TypeScript CMS resolution consistent and avoids relative path gymnastics across workspaces.

Managing Cross-Workspace Dependencies

Your package manager, npm, Yarn, pnpm, or Bun, treats every workspace as part of one dependency graph. Run install at the repository root so all apps and packages resolve together. One install command synchronizes all apps, libraries, and CI pipelines.

Local and Remote Caching

Turborepo stores local cache data under .turbo in the repository root. Add it to .gitignore:

# .gitignore
.turbo/

Here's what cache hits and misses look like in your terminal:

$ turbo run build
• Packages in scope: @acme/blog, @acme/docs, @acme/ui
• Running build in 3 packages
@acme/ui:build: cache hit, replaying logs  ██████████
@acme/blog:build: cache miss, executing    ██████████
@acme/docs:build: cache hit, replaying logs ██████████

 Tasks:    3 successful, 3 total
 Cached:   2 cached, 3 total
   Time:   4.2s

Setting Up Remote Caching

Authenticate and link your repo to share cache artifacts across your team and CI:

npx turbo login        # authenticate with Vercel
npx turbo link         # connect this repo to remote cache

In CI, set two environment variables, TURBO_TOKEN and TURBO_TEAM, and Turborepo handles the rest. Use a repository variable (not a secret) for TURBO_TEAM to prevent GitHub Actions from censoring the team name in logs.

You can verify remote caching works by deleting your local .turbo/cache directory and running the build again. If artifacts replay from the remote store, you're set. Run turbo config to check your remote cache status at any time.

For teams that can't use Vercel, turbo login --manual accepts a custom API URL. Any HTTP server implementing Turborepo's Remote Caching API works as a backend.

Development Workflow with Watch Mode

Turborepo v2 introduced turbo watch, which re-runs tasks when files change. This is dependency-aware. Tasks re-run in the order defined in your turbo.json, not just when their own files change:

turbo watch dev lint test

For tools with built-in watchers like next dev, mark the task as persistent: true and let the framework handle hot reloading. For tools without monorepo-aware watchers, mark the task as both persistent: true and interruptible: true so turbo watch can kill and restart the task when upstream dependencies change:

{
  "tasks": {
    "dev": {
      "cache": false,
      "persistent": true,
      "interruptible": true,
      "dependsOn": ["^build"]
    }
  }
}

Interactive Tasks and Terminal UI

The Terminal UI (TUI) shipped as stable in 2.0 and is the default interface. Navigate between tasks with arrow keys, press Enter to attach to a task's interactive shell, and CTRL+Z to detach.

This enables workflows like running Jest or Vitest in watch mode directly from the TUI:

{
  "tasks": {
    "test:watch": {
      "interactive": true,
      "persistent": true
    }
  }
}

Enable the TUI explicitly in turbo.json with "ui": "tui", though it's already the default in v2.

CI/CD Integration and Selective Deployment

Integrate Turborepo into your CI pipeline after local setup works. A push triggers the pipeline, the runner pulls the remote cache, executes affected tasks, and pushes fresh artifacts back.

GitHub Actions Pipeline

This v2-compatible workflow uses pnpm, based on the CI guide:

name: CI

on:
  push:
    branches: ["main"]
  pull_request:
    types: [opened, synchronize]

jobs:
  build:
    name: Build and Test
    timeout-minutes: 15
    runs-on: ubuntu-latest
    # To use Remote Caching, uncomment the next lines and follow the steps below.
    # env:
    #   TURBO_TOKEN: ${{ secrets.TURBO_TOKEN }}
    #   TURBO_TEAM: ${{ vars.TURBO_TEAM }}

    steps:
      - name: Check out code
        uses: actions/checkout@v4
        with:
          fetch-depth: 2

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

      - name: Setup Node.js environment
        uses: actions/setup-node@v4
        with:
          node-version: 20
          cache: 'pnpm'

      - name: Install dependencies
        run: pnpm install

      - name: Lint, type-check, and test
        run: turbo run lint check-types test

      - name: Build web app
        run: turbo run build --filter=web

The fetch-depth: 2 gives Turborepo access to the previous commit for change detection. Content-based cache keys restore instantly on untouched code paths, reducing CI minutes and runner costs.

Selective Deployment with --filter

The --filter flag targets specific workspaces and their dependencies:

turbo run build --filter=@apps/blog          # by package name
turbo run build --filter=web...              # package + all its dependencies
turbo run build --filter=[HEAD^1]            # packages changed since last commit

For smarter change detection, Turborepo v2 replaces the deprecated turbo-ignore package with turbo query affected. Use the --affected flag for the simplest approach, or affected packages for conditional deployment steps:

- name: Check if web is affected
  id: web-affected
  run: turbo query affected --packages web --exit-code
  continue-on-error: true

- name: Deploy web
  if: steps.web-affected.outcome == 'failure'
  run: turbo run deploy --filter=web

The exit code behavior is inverted from what you might expect: code 1 means the package IS affected (proceed with deploy), and code 0 means it isn't. With Strapi 5, content webhooks can trigger the same filtered build command for whichever frontend consumes that content.

Building Multiple Frontends with Strapi and Turborepo

Strapi delivers content via REST and GraphQL APIs, but coordinating multiple frontends that consume the same content creates overhead. Turborepo solves this with a shared SDK package. Create one workspace for your typed API client, update your Strapi content model, regenerate types, and Turborepo rebuilds only affected frontends.

The Strapi community repository, itself a Turborepo monorepo, demonstrates the canonical pattern: wrap @strapi/client in a shared workspace package that layers your content-type interfaces on top.

├── apps/
│   ├── web/              # Next.js
│   └── marketing/        # Astro
├── packages/
│   └── strapi-client/
│       ├── src/
│       │   ├── client.ts
│       │   ├── types/
│       │   │   └── article.ts
│       │   └── index.ts
│       └── package.json
├── cms/                  # Strapi v5 backend
└── turbo.json

Your typed client factory lets each app inject its own environment configuration:

// packages/strapi-client/src/client.ts
import { strapi } from '@strapi/client';
import type { Article } from './types/article';

export function createStrapiClient(config: {
  baseURL: string;
  auth?: string;
}) {
  const client = strapi({
    baseURL: config.baseURL,
    auth: config.auth,
  });

  return {
    articles: client.collection<Article>('articles'),
    raw: client,
  };
}

Strapi 5 uses a flattened response format. Attributes sit directly on each data object, and documentId replaces id as the stable identifier. Your type definitions reflect this:

export interface Article {
  id: number;
  documentId: string;
  title: string;
  slug: string;
  content: string;
  createdAt: string;
  updatedAt: string;
}

Both your Next.js and Astro frontends consume the same package:

// apps/web/lib/strapi.ts
import { createStrapiClient } from '@repo/strapi-client';

export const cms = createStrapiClient({
  baseURL: process.env.STRAPI_API_URL!,
  auth: process.env.STRAPI_API_TOKEN,
});

The result is one Strapi backend, shared type safety via a single SDK package, and selective deployments. Strapi, as a headless CMS, handles multi-tenancy scenarios, while Turborepo coordinates builds and deploys only changed applications. This reduces coordination overhead across frontends.

Paul BratslavskyDeveloper Advocate

Related Posts

 What Is An AI Code Editor?
Ecosystem·15 min read

What Is An AI Code Editor? + the 6 Best Ones

AI code editors enhance productivity through smart suggestions, automation, and error catching. These features enable...

·June 4, 2025
Vibe Coding Explained
Ecosystem·21 min read

Vibe Coding Explained for Developers Who Hate Wasting Time on Boilerplate

Discover vibe coding - the AI development method that turns natural language prompts into production code and boosts developer productivity greatly.

·October 15, 2025
TutorialsBeginner·18 min read

How to Deploy a Strapi-Powered Gatsby Blog on Cloudflare Pages

Learn how to deploy Gatsby on Cloudflare Pages with the Strapi API on Render and use webhooks for automatic redeploys.

·December 15, 2022