Back to the blog
Toolingยท Jun 28, 2026ยท 7 min read

Turborepo monorepo in practice: structure, caching, and the mistakes to skip

Remote caching is free on Vercel. pnpm workspaces + Turborepo is the settled stack. Here's the folder structure, turbo.json, and the three decisions that actually matter.

#turborepo#monorepo#dx#tooling

Monorepos have a reputation for complexity that's mostly earned by the teams that adopt them prematurely. The case for a monorepo is simple: if you have two or more apps sharing code - a component library, shared utilities, a type layer - a monorepo eliminates the publish-and-bump cycle entirely and lets you make atomic cross-package changes in a single commit. Turborepo is the tool we reach for and the one the JavaScript ecosystem has settled on for most use cases. Here's how we structure it.

The three-layer folder structure

The structure that's emerged as the standard across the community is three directories: `apps/` for deployable applications (Next.js, Vite SPAs, API services), `packages/` for shared code (component libraries, utils, shared types, config), and `tooling/` for shared development configuration (ESLint config, TypeScript base configs, Tailwind presets). Apps should be thin - they consume packages, they don't own business logic. Packages are where shared complexity lives.

monorepo/
โ”œโ”€โ”€ apps/
โ”‚   โ”œโ”€โ”€ web/          # Next.js or TanStack Start app
โ”‚   โ””โ”€โ”€ api/          # Hono or Express API service
โ”œโ”€โ”€ packages/
โ”‚   โ”œโ”€โ”€ ui/           # Shared component library
โ”‚   โ”œโ”€โ”€ db/           # Drizzle/Prisma schema + client
โ”‚   โ””โ”€โ”€ validators/   # Zod schemas shared by api and web
โ””โ”€โ”€ tooling/
    โ”œโ”€โ”€ eslint/       # @acme/eslint-config package
    โ””โ”€โ”€ typescript/   # Base tsconfig.json files

turbo.json: the three pipeline decisions that matter

Turborepo's task pipeline is where most configuration time gets spent. Three decisions drive most of the behaviour: which tasks depend on `^build` (meaning the task must run in dependency order - typically `build` and `typecheck`), which tasks are cacheable (almost everything that has deterministic output), and which tasks should run in watch mode (`dev` tasks should never be cached). Getting these right means fast CI and a sensible local dev experience.

// turbo.json
{
  "tasks": {
    "build": {
      "dependsOn": ["^build"],
      "outputs": [".next/**", "dist/**"]
    },
    "typecheck": {
      "dependsOn": ["^build"]
    },
    "lint": {
      "dependsOn": []
    },
    "dev": {
      "cache": false,
      "persistent": true
    }
  }
}

Remote caching

Turborepo's remote cache shares build artifacts across machines - your CI pipeline hits the same cache your local machine populated. On Vercel, remote caching is free for repos linked to a Vercel project (since late 2024); for self-hosted setups, Turborepo supports a remote cache server that you can run on any S3-compatible storage. The practical effect: after the first CI run on a given commit, subsequent runs for PRs that haven't changed a given package skip its build entirely. On a monorepo with eight packages, this cuts CI time by 60โ€“70% after the cache is warm.

The mistakes worth avoiding

  • Creating too many packages too early - start with three to five packages, not twenty. Each package boundary adds overhead (versioning, build order, TS paths). Extract only when the sharing need is concrete, not anticipated.
  • Putting business logic in apps/ - apps should be routing, layout, and composition. Logic that might be shared belongs in packages/ even if only one app uses it today.
  • Forgetting to set `dependsOn: ["^build"]` on typecheck - without this, TypeScript will check your app against un-built package types and produce confusing errors.
  • Using `npm workspaces` instead of pnpm - pnpm's `workspace:` protocol and strict hoisting behavior catch dependency issues that npm silently masks through its flat node_modules structure.

Written by Appesto Engineering.