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. Fifteen-minute builds complete in two minutes.
This guide walks through Turborepo setup, task configuration, and deployment for multiple frontends sharing code.
In Brief:
Turborepo optimizes build performance in JavaScript monorepos 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, and reduce build times when remote caching runs across local machines and CI runners.
Turborepo executes independent tasks in parallel based on the dependency graph you define in turbo.json, waiting only when one package depends on another.
This approach eliminates the multi-repo overhead of publishing internal packages to npm, synchronizing versions, and rebuilding identical code in separate pipelines.
Monorepos work best when projects share code and deploy together. The caching advantage grows with the amount of shared dependencies across your applications.
A monorepo keeps all your related projects—applications, shared libraries, configuration—in one version-controlled repository. Instead of juggling several Git remotes, you point your team at a single source of truth where every change lives side by side.
Companies like Google and Facebook rely on this pattern for its coordination benefits, and you can explore the approach in depth at monorepo. tools.
A Next.js blog, Gatsby docs site, and React marketing page share UI kits and TypeScript types. Place shared packages under packages/. Update a button's API, commit once, and all three apps receive the change immediately—no package publishing, no version bumps.
Your root package.json declares every workspace (for npm and Yarn), keeps the repository private, and centralizes shared tooling; for pnpm, workspaces are instead declared in pnpm-workspace.yaml.
1.
2├── apps
3│ ├── blog
4│ ├── docs
5│ └── marketing
6├── packages
7│ ├── ui
8│ ├── types
9│ └── api-client
10├── package.json # workspaces list lives here
11└── turbo.json # build pipelinesContrast that with a polyrepo setup: every app sits in its own repo, each with duplicated dependencies, independent CI pipelines, and manual cross-project coordination.
Unified repositories eliminate that overhead by enabling atomic commits, consistent tooling, and straightforward dependency management.
The tradeoff is scale. Without tooling like the build orchestrator, large unified repos suffer slow builds and complex CI/CD.
The build system's speed comes from three mechanisms working together: task caching, dependency-aware execution, and parallel processing.
Task caching eliminates redundant work. The tool hashes everything that influences a task—source files, environment variables, compiler flags—then stores the resulting artifacts locally and optionally in a remote cache.
When inputs remain unchanged, it restores cached output instead of rebuilding. Build times drop by roughly 85 percent after enabling caching in large repositories.
Dependency-aware execution ensures correct build order. You declare task relationships in turbo.json, and the system automatically builds a dependency graph to determine the execution sequence. Your UI package always builds before the applications that import it, eliminating "module not found" errors:
1{
2 "pipeline": {
3 "build": {
4 "dependsOn": ["^build"],
5 "outputs": ["dist/**"]
6 },
7 "test": {
8 "dependsOn": ["build"]
9 }
10 }
11}Parallel processing runs independent tasks simultaneously. A 15-minute build completes in 2 minutes when tasks run in parallel and results are cached.
Task caching, dependency graphs, and parallel execution combine to cut wait time. Builds finish in minutes instead of blocking your workflow.
Turborepo solves coordination problems when multiple applications share code. If your blog, docs site, and marketing page all pull from the same Strapi backend 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. The caching layer rebuilds only what has changed in large codebases.
Use Turborepo when you:
Skip monorepos when your projects:
If you frequently make changes that affect multiple repositories, or if keeping shared code in sync creates friction, Turborepo is a good fit.
If your projects evolve independently with minimal shared dependencies, stick with separate repos.
Turborepo skips redundant work through content-aware caching, consolidates dependency management, and deploys only changed applications.
--filter flag. CI rebuilds only affected applications while restoring cached artifacts for everything else.Turborepo combines content-aware caching, parallel task execution, remote artifact sharing, and incremental builds to eliminate redundant work in JavaScript monorepos.
The local cache eliminates redundant work by storing task outputs after every run.
When you execute a task, the system creates a hash from your input source files, package.json dependencies, environment variables, and build configuration.
If nothing changes, subsequent runs restore cached artifacts in milliseconds, rather than rebuilding from scratch.
This content-based hashing approach means you never get stale builds. Change a single line, and the tool rebuilds only the affected workspace.
Everything else gets restored from cache instantly. Teams typically see 85% reductions in build time when cache hits become the norm.
Your turbo.json configuration tells the system what to cache:
1{
2 "pipeline": {
3 "build": {
4 "outputs": ["dist/**"]
5 },
6 "test": {
7 "dependsOn": ["build"],
8 "outputs": ["coverage/**"]
9 }
10 }
11}The outputs array specifies which directories contain your build artifacts. The caching system stores these folders and restores them when the inputs match those from previous runs.
The dependsOn ensures tests run after builds complete, while still benefiting from caching when source files remain unchanged.
With this configuration, your turbo run build test commands skip unchanged work automatically. You code, commit, and see results—without waiting for unnecessary rebuilds.
The build orchestrator treats every task as a node in a dependency graph you define in turbo.json. Declare how tasks depend on one another, and you guarantee builds happen in the correct order while independent workspaces run in parallel.
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 the rest of your repo.
1{
2 "pipeline": {
3 "build": {
4 "dependsOn": ["^build"], // build this workspace's dependencies first
5 "outputs": ["dist/**"]
6 },
7 "test": {
8 "dependsOn": ["build"] // run tests after the local build completes
9 }
10 }
11}The caret (^build) tells the system to run the build task in every dependent workspace before running the build task in the current workspace.
The tool understands the entire graph and schedules unrelated tasks concurrently, eliminating idle CPU time.
Remote caching pushes build artifacts to a shared store so your entire team—and CI pipeline—can reuse them instead of repeating work. When you finish a build locally, the system uploads the hashed outputs.
Your teammate pulls the branch and runs turbo run build—instead of waiting minutes, they get a near-instant cache hit. CI skips unchanged workspaces entirely, cutting build times in large repositories.
Setup requires two commands: npx turbo login to get your token, and npx turbo link to connect the repo.
Turborepo defaults to its own local cache, but you can manually configure integration with Vercel Remote Cache or set up custom remote caching using additional tools or scripts.
Cache sharing begins with your first build after linking. New team members skip initial builds, CI costs drop, and deployments complete faster.
When you run a task with the build system, it creates a content-aware hash of every relevant input —your source files, dependencies, and configuration — for each workspace.
If that hash matches an entry in the cache, the tool instantly restores the previously generated output instead of rebuilding.
Otherwise, the task runs and its result gets cached for next time. Content-aware hashing analyzes file contents rather than timestamps, so you avoid the false positives that plague simpler incremental build tools.
This approach scales as your repository grows. Touch a single package in a unified repository with 100 applications, and only the modified package plus its direct dependents rebuild—everything else gets skipped.
Configure your pipelines once, and every subsequent commit skips unchanged work automatically.
You don't need to start fresh; drop the build optimizer into your existing codebase and watch it start caching builds and tests immediately.
Install the CLI with npm install -D turbo, add a turbo.json file at your repo root, and define tasks like build, dev, and test.
The system analyzes dependencies, stores outputs locally, and skips work when inputs haven't changed.
Structure your workspace around intent: deployable apps go in apps/ for your blog, docs, and marketing sites, while shared code lives in packages/ for UI components or your Strapi API client.
Since the tool supports npm, Yarn, and pnpm workspaces, you manage dependencies exactly as before —only faster.
When you're ready to ship:
1turbo run build --filter=@apps/blogOnly the changed application and its dependencies are rebuilt, so your CI/CD pipeline deploys in minutes instead of waiting for the entire repository.
Start by defining clear boundaries between deployable applications and reusable packages. Put your applications in apps/ and shared code in packages/.
Your root package.json declares every workspace (for npm and Yarn), keeps the repository private, and centralizes shared tooling; for pnpm, workspaces are instead declared in pnpm-workspace.yaml:
1{
2 "name": "company-monorepo",
3 "private": true,
4 "workspaces": ["apps/*", "packages/*"],
5 "scripts": {
6 "dev": "turbo run dev --parallel",
7 "build": "turbo run build"
8 },
9 "devDependencies": {
10 "turbo": "latest",
11 "typescript": "^5.3.0"
12 },
13 "dependencies": {
14 "react": "^18.2.0"
15 }
16}Your workspace structure then looks like:
1.
2├── apps
3│ ├── blog
4│ ├── docs
5│ └── marketing
6└── packages
7 ├── api-client
8 ├── types
9 ├── ui
10 ├── strapi-sdk
11 └── strapi-typesYou build and deploy applications in apps/ . Applications consume code from packages/ through workspace dependencies.
With this structure, the build system caches outputs per package, rebuilds only changed code, and keeps your Strapi SDK synchronized across every frontend without manual coordination.
The turbo.json file defines how tasks execute across your repository. Declare task outputs and dependencies, and Turborepo executes everything in the correct order while skipping cached work.
1{
2 "$schema": "https://turborepo.com/schema.json",
3 "pipeline": {
4 "dev": {
5 "cache": false
6 },
7 "build": {
8 "dependsOn": ["^build"],
9 "outputs": ["dist/**"]
10 },
11 "lint": {
12 "outputs": []
13 },
14 "test": {
15 "dependsOn": ["build"],
16 "outputs": ["coverage/**"]
17 },
18 "generate-types": {
19 "dependsOn": [],
20 "outputs": ["src/__generated__/**"]
21 }
22 }
23}The dependsOn field controls task execution order. ^build creates all workspace dependencies before the current package. For unit tests, "build" run the local build first. The outputs field defines which artifacts Turborepo caches. Unchanged inputs restore cached outputs instantly. Set cache: false on development servers to preserve hot reloading.
In Strapi repositories, add a generate-types step before build to compile frontend packages against your latest CMS schema. Adjust task names or output folders for your stack—Next.js, Gatsby, or Node.js—and Turborepo handles execution and caching.
Inside your unified repository, you treat reusable code as "internal packages." These packages live in the same repository, so you skip the publish-to-npm workflow and let workspace tooling handle everything.
Your blog, docs, and admin apps can all import from the same Strapi client without version conflicts—one of the core productivity wins that makes unified repositories compelling.
Here's how to structure an internal package:
1packages/strapi-sdk/
2├─ src/
3│ ├─ client.ts
4│ └─ index.ts
5└─ package.jsonYour package manifest declares a name and explicit export map:
1{
2 "name": "@acme/strapi-sdk",
3 "version": "0.1.0",
4 "main": "dist/index.js",
5 "exports": {
6 ".": "./dist/index.js",
7 "./auth": "./dist/auth.js"
8 }
9}Add the SDK to any application workspace using the workspace shortcut:
1npm add @acme/strapi-sdk@workspace:^Import the SDK into your applications:
1import { createClient } from '@acme/strapi-sdk';When you modify the SDK, the content-aware hashing rebuilds and tests only the applications that import it. This keeps your iteration cycle fast while ensuring consistent logic everywhere.
Your package manager—npm, Yarn, or pnpm—treats every workspace as part of one dependency graph. Run install at the repository root, and shared libraries hoist to the root node_modules. Each application references the same copy, eliminating duplicate installations and aligning versions.
When two workspaces need different versions, the package manager nests the older version locally. The rest of your repository shares the hoisted copy. One install command synchronizes all apps, libraries, and CI pipelines.
Turborepo writes task outputs to a .turbo directory in your project root. The system hashes each task's inputs—source files, environment variables, lockfiles—and invalidates cache entries when anything changes. This prevents stale artifacts. Add the directory to your version-control ignore list.
1# .gitignore
2.turbo/Local caching works on your machine, but teams need shared artifacts. Remote caching stores build outputs in a cloud cache. After installing Turborepo, authenticate and connect your repository.
1npx turbo login # authenticate
2npx turbo link # register this repo with the remote cacheTurborepo stores compressed build artifacts in the remote backend. Vercel offers a zero-config option, or you can configure S3, Google Cloud Storage, or any custom server. In continuous integration, set two environment variables—TURBO_TEAM and TURBO_TOKEN—to fetch and push cache entries.
Remote caching turns a 15-minute full build into a sub-minute cache restore for unchanged workspaces. Once one developer builds, everyone else skips the work across laptops and CI runs.
Rebuilding your entire unified repository for a single file change wastes time and resources. The build system's --filter flag targets specific workspaces and their dependencies, leaving everything else untouched.
1# build only the web app and everything it depends on
2turbo run build --filter=apps/webIn GitHub Actions, wire this into your deployment pipeline:
1- name: Build & deploy changed app
2 run: |
3 turbo run build --filter=${{ env.CHANGED }}
4 turbo run deploy --filter=${{ env.CHANGED }}
5 env:
6 CHANGED: ${{ steps.turbo.outputs.filtered }}Turborepo's change detection produces the filtered output. When nothing changes in apps/web, the job restores cached artifacts from your remote store and finishes instantly.
With Strapi, content webhooks trigger the same filtered build command for whichever frontend consumes that content. Unrelated applications remain untouched.
Selective deployment converts full-repo builds into deterministic tasks that finish in seconds instead of minutes.
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.
Add TURBO_TOKEN and TURBO_TEAM secrets to your CI provider—Turborepo picks them up automatically. The official CI guide covers all major vendors. For GitLab, use the drop-in snippet in the GitLab documentation section.
Paste this minimal GitHub Actions workflow into .github/workflows/ci.yml
1name: CI
2on: [push, pull_request]
3
4jobs:
5 build:
6 runs-on: ubuntu-latest
7 steps:
8 - uses: actions/checkout@v3
9 - uses: actions/setup-n
10ode@v3
11 - run: npm ci
12 - run: turbo run build test --cache-dir=.turbo
13 env:
14 TURBO_TOKEN: ${{ secrets.TURBO_TOKEN }}
15 TURBO_TEAM: ${{ secrets.TURBO_TEAM }}The job installs dependencies once, then delegates task orchestration and parallel execution to Turborepo. Content-based cache keys return instantly on untouched code paths, reducing CI minutes and runner costs. The same pattern works on other platforms—export the two environment variables and invoke turbo run in your build step.
Strapi delivers content via APIs, but coordinating multiple frontends creates overhead. Turborepo solves this with shared SDK packages. Create one workspace for API clients, TypeScript types, and authentication helpers. Update your Strapi content model, regenerate types once, and Turborepo rebuilds only affected frontends.
The same SDK powers your Next.js blog, Gatsby docs, and React marketing site. Type definitions sync automatically across every frontend, and version conflicts disappear.
The result: one Strapi backend, shared type safety, and selective deployments. Strapi handles your content while Turborepo coordinates builds and deploys only changed applications. Ship features instead of synchronizing repositories.