Monorepo & Turborepo Review
Purpose
Review the structure, pipeline configuration, caching strategy, task dependency graph, shared package design, versioning, and build correctness of monorepos managed with Turborepo (and associated tools: pnpm/npm/yarn workspaces, Changesets, tsconfig path aliases).
When to use
- Reviewing a
turbo.jsonpipeline configuration and itsdependsOn,inputs,outputsdefinitions. - Auditing workspace package structure: shared UI libraries, utility packages, config packages.
- Evaluating cache hit rate issues: builds that should be cached but are not, or incorrect cache hits serving stale output.
- Reviewing Changesets versioning workflow for a monorepo publishing to npm.
- Checking
tsconfig.jsonpath aliases and composite project references in a monorepo. - Evaluating task ordering and circular dependency issues across packages.
When not to use
- Single-package repository — no workspace or build graph concerns apply.
- Nx-based monorepo — similar concepts but different config schema; note divergences.
- Docker/container build review unless specifically about how Docker interacts with the Turborepo build output.
Procedure
1. Workspace structure
- Root
package.jsonmust defineworkspaces(npm/yarn) orpnpm-workspace.yamlmust be present (pnpm) listing all package globs (e.g.,packages/*,apps/*). - Package naming convention: use a consistent scope prefix (
@myorg/ui,@myorg/config,@myorg/utils) — avoid plain unscoped names for internal packages. - Directory layout: separate
apps/(deployable applications) frompackages/(shared libraries) fromtooling/(shared configs: eslint, tsconfig, prettier). This separation clarifies what is published vs consumed internally. - Each package must have its own
package.jsonwithname,version, andprivate: truefor non-published packages. - Do not commit
node_modules/in any package — a single root install viapnpm install/npm installmust cover all workspaces.
2. turbo.json pipeline
turbo.jsonat the root defines global pipeline; package-levelturbo.json(v2) can extend or override.dependsOn: ["^build"]means: runbuildfor all upstream dependencies first. This is the most critical field — missing it causes stale dependency output to be used.- Task input/output definitions control cache correctness:
inputs: list file globs that, when changed, invalidate the cache. Default is all tracked files in the package — this is safe but conservative. Narrow inputs (e.g.,["src/**", "tsconfig.json"]) improve cache hit rate.outputs: list the files the task produces (e.g.,[".next/**", "dist/**"]). Missing outputs means Turborepo cannot restore them from cache.cache: falseon tasks likedev(watch mode) andtest:watch— these must never be cached.persistent: trueon long-running tasks (dev,start) — tells Turborepo not to wait for them to complete before running dependents.
3. Build dependency graph
- Run
turbo run build --dry=jsonto inspect the task graph. Review: - Is the execution order correct? Shared packages must build before apps that consume them.
- Are there any circular dependencies? Turbo will error on cycles — they must be resolved.
- Are tasks being parallelized where possible? Packages with no dependency on each other should run concurrently.
- Circular dependency detection: if a package A imports from package B and package B imports from package A, the build order is undefined. Use
madge --circularorturbo run build --dry=jsonto identify. - Implicit dependencies: if an app imports a shared package but the app's
package.jsondoes not list it as a dependency, the task graph does not know to build it first. Always declare explicitdependenciesinpackage.json, even for internal workspace packages.
4. Cache correctness
- False cache hit (serving stale output): occurs when
inputsis too narrow and misses a file that actually affects the output. Test by: change a file that should affect the build, then run — if Turborepo reports a cache hit, theinputsdefinition is too narrow. - Cache miss on every run (no hits): occurs when
inputsinadvertently includes files that always change (e.g., generated files,.envfiles, build timestamps written to source). Narrowinputsto only source and config files. - Environment variable handling: variables that affect the build (e.g.,
NODE_ENV,NEXT_PUBLIC_API_URL) must be listed inturbo.jsonunderglobalEnvor task-levelenv. Unlisted env vars are invisible to the cache key — changing them will not invalidate the cache, causing incorrect cached output to be served. turbo.jsonglobalDependencies: list files whose change should invalidate all task caches (e.g., roottsconfig.json, root.eslintrc,pnpm-lock.yaml).
5. Remote cache
- Vercel Remote Cache (or self-hosted): enables CI cache sharing across machines and branches.
- Authenticate with
TURBO_TOKENandTURBO_TEAMenvironment variables in CI. - Remote cache must be enabled in
turbo.json:"remoteCache": { "enabled": true }(Turborepo v2). - Cache signatures: by default the remote cache is unsigned. For security-sensitive builds, enable artifact signing with
--cache-dir+ signature verification to prevent cache poisoning. - Never store secrets in task outputs that are cached — cached artifacts are accessible to anyone with the cache token.
6. Shared packages
- Config packages (
@myorg/tsconfig,@myorg/eslint-config): must export their configs viapackage.jsonexportsfield, not just through file paths. Using direct paths breaks when packages are published. - UI packages (
@myorg/ui): decide on a compilation strategy — source distribution (consumer compiles) vs compiled distribution (pre-built). Source distribution is simpler for internal monorepos; compiled is required for external npm publishing. tsconfig.jsonin each package: shouldextendfrom a base config package (@myorg/tsconfig/base.json). Every package that is referenced by another must have"composite": truein its tsconfig for TypeScript project references to work.package.jsonexportsmap: if a package exports multiple entry points, define them in theexportsfield — do not rely on directdist/path imports which are fragile and break with bundler resolution.- Peer dependencies: shared packages should declare React, TypeScript, etc. as
peerDependencies, notdependencies, to avoid duplicate installations in consuming apps.
7. Versioning and publishing with Changesets
.changeset/directory must exist at the root; each PR that changes a publishable package adds a.mdchangeset file describing the change (patch/minor/major).changeset versionbumps package versions and updates changelogs based on accumulated changesets.changeset publishpublishes to npm — must only run in CI on a release branch afterchangeset versionhas committed the version bumps.private: truepackages must never be published — verify thepublishpipeline excludes allprivatepackages.- Linked packages: if packages share a version (e.g.,
@myorg/uiand@myorg/tokensalways release together), configure them aslinkedin.changeset/config.json. - Pre-release: Changesets supports pre-release mode (
changeset pre enter alpha) — verify it is exited (changeset pre exit) before the final release.
8. CI integration
- Turborepo prune: use
turbo prune --scope=@myorg/app --dockerto generate a minimal lockfile for Docker builds that includes only the transitive dependencies of one app — drastically reduces Docker layer size. --filter: useturbo run build --filter=@myorg/app...to run only the build for one app and its dependencies in CI when only that app changed.--since=origin/main:turbo run build --filter=...[origin/main]runs tasks only for packages changed since the base branch — enables affected-only CI.TURBO_CONCURRENCY: set to the number of available CPU cores in CI to maximize parallelism.- Verify the
turbobinary version is pinned indevDependencies— floatinglatestcauses unexpected behavior changes after Turborepo releases.
Checklist
Workspace:
-
workspacesorpnpm-workspace.yamldefined at root. - Consistent
@scope/naming for all internal packages. -
apps/andpackages/directories separated. - No
node_modules/committed.
turbo.json:
-
dependsOn: ["^build"]on all tasks that consume upstream output. -
inputsdefined and excludes generated/always-changing files. -
outputsdefined for all tasks that produce artifacts. -
envlists all environment variables that affect the build. -
globalDependenciesincludes root config files. -
cache: falseon watch/dev tasks.
Cache:
- Remote cache configured with
TURBO_TOKENandTURBO_TEAMin CI. - No secrets in cached task outputs.
- Cache invalidates correctly when env vars change.
Shared packages:
-
exportsfield defined in each shared package'spackage.json. -
"composite": truein tsconfig for referenced packages. - React/TypeScript in
peerDependencies, notdependencies.
Versioning:
-
.changeset/config.jsonpresent and configured. - CI publishes only after
changeset versioncommits. - All
private: truepackages excluded from publish.
CI:
-
turboversion pinned indevDependencies. -
--filter=[origin/main]used for affected-only runs. -
turbo pruneused for Docker builds.
Common issues & anti-patterns
- Missing
dependsOn: ["^build"]: app builds before its shared package dependency finishes building — uses stale or missing compiled output. The build appears to succeed in CI but deploys broken code. - Unlisted env var in
turbo.jsonenv:NEXT_PUBLIC_API_URLchanges between staging and production but is not inenv. Turborepo serves a cached build configured for staging to production. - Generated files in
inputs: a codegen step writes tosrc/generated/andinputsincludessrc/**. Every codegen run changes these files, busting the cache on every CI run. - Circular workspace dependency:
@myorg/uiimports from@myorg/hooks, and@myorg/hooksimports from@myorg/ui. Turborepo errors; the packages need to be restructured. - Implicit internal dependency: an app uses
import { Button } from '@myorg/ui'but@myorg/uiis not in the app'spackage.json. The task graph does not build@myorg/uifirst — works locally (hoisted) but breaks in CI with strict install mode. private: truepackage accidentally published: ifchangeset publishis run without a proper package filter, an internal config package gets published to npm.- No remote cache in CI: each CI run re-builds everything from scratch. A monorepo with 20 packages takes 15 min; with remote cache sharing, it takes 2 min on the second run.
tsconfigwithoutcomposite: truein referenced package: TypeScript project references fail to build incrementally —tsc -brebuilds the entire dependency chain instead of using cached.tsbuildinfofiles.
Required output
Return a structured report with:
- Summary: pass / needs fixes / blocked (build correctness, cache poisoning risk, publish safety).
- Task graph snapshot: key tasks, their
dependsOn, and whether the execution order is correct. - Cache analysis: inputs/outputs correctness, env var coverage, estimated cache hit rate impact.
- Findings table: severity (critical / high / medium / low / info), category (pipeline / cache / packages / versioning / CI), file + line, description, remediation.
- Shared package assessment: exports map, tsconfig composite, peer dependency status.
- Versioning safety: changeset config, publish guard status.
- Next handoff: run
turbo run build --dry=jsonto validate graph; test cache hit with a known-unchanged package; verify--filter=[origin/main]in CI pipeline.
Safety
- Do not run
changeset publishornpm publishduring review. - Do not modify
pnpm-lock.yamlorpackage-lock.json— lockfile changes affect all packages and must go through the normal PR process. - Do not clear the remote cache — it affects all team members' CI performance.
- If secrets are found in
turbo.jsonglobalEnvvalues (hardcoded, not just variable names) or in cached artifact directories, flag as critical.