Hunchbite
ServicesGuidesCase StudiesAboutContact
Start a project
Hunchbite

Software development studio focused on craft, speed, and outcomes that matter. Production-grade software shipped in under two weeks.

+91 90358 61690hello@hunchbite.com
Services
All ServicesSolutionsIndustriesTechnologyOur ProcessFree Audit
Company
AboutCase StudiesWhat We're BuildingGuidesToolsPartnersGlossaryFAQ
Popular Guides
Cost to Build a Web AppShopify vs CustomCost of Bad Software
Start a Project
Get StartedBook a CallContactVelocity Program
Social
GitHubLinkedInTwitter

Hunchbite Technologies Private Limited

CIN: U62012KA2024PTC192589

Registered Office: HD-258, Site No. 26, Prestige Cube, WeWork, Laskar Hosur Road, Adugodi, Bangalore South, Karnataka, 560030, India

Incorporated: August 30, 2024

© 2026 Hunchbite Technologies Pvt. Ltd. All rights reserved.· Site updated February 2026

Privacy PolicyTerms of Service
Home/Guides/Setting Up a Next.js Monorepo with Turborepo: A Production-Ready Guide
Guide

Setting Up a Next.js Monorepo with Turborepo: A Production-Ready Guide

How to structure a Next.js monorepo using Turborepo — workspace setup, shared packages, TypeScript configuration, CI caching, and the decisions that matter when the repo scales beyond one team.

By HunchbiteMarch 30, 202612 min read
TurborepoNext.jsmonorepo

Monorepos are the right architecture for most multi-package JavaScript projects. Turborepo makes them fast.

This guide covers a production-ready setup: workspace structure, shared packages, TypeScript configuration, CI with remote caching, and the configuration decisions that matter as the repo grows.

Why Turborepo

A monorepo without a build system is a slow monorepo. Running tsc and next build across ten packages serially is slower than running them across separate repos. Turborepo fixes this with two things:

Task parallelisation. Tasks that don't depend on each other run simultaneously. If building web and docs don't depend on each other, they build in parallel.

Caching. Tasks that haven't changed don't re-run. Their output is served from cache — locally or from a remote cache shared across the whole team and CI.

The result: a 10-package monorepo that naively takes 8 minutes to build runs in 90 seconds with Turborepo after the first full build.

Project structure

my-project/
├── apps/
│   ├── web/          # Next.js main application
│   └── docs/         # Next.js docs or marketing site
├── packages/
│   ├── ui/           # Shared React components
│   ├── typescript-config/  # Shared tsconfig files
│   └── eslint-config/      # Shared ESLint config
├── package.json
├── pnpm-workspace.yaml
└── turbo.json

apps/ contains deployable applications — things that run as a service. packages/ contains shared code — things that apps import.

Initial setup

Prerequisites

node --version  # 18+
pnpm --version  # 8+

Use pnpm. The disk efficiency and workspace linking is significantly better than npm or yarn for monorepos.

Initialise the monorepo

mkdir my-project && cd my-project
pnpm init

Create pnpm-workspace.yaml:

packages:
  - "apps/*"
  - "packages/*"

Install Turborepo:

pnpm add -D turbo --workspace-root

Configure Turborepo

turbo.json at the root:

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

Key points:

  • "dependsOn": ["^build"] means "build all dependencies first." The ^ prefix means "in dependent packages."
  • "inputs" tells Turbo what files affect this task's cache key. Include .env* if env vars affect output.
  • "outputs" tells Turbo what to cache. .next/cache/** is excluded because it's Next.js's own build cache, not output.

Shared TypeScript configuration

Create packages/typescript-config/ with:

package.json:

{
  "name": "@repo/typescript-config",
  "version": "0.0.0",
  "private": true,
  "files": ["*.json"]
}

base.json:

{
  "$schema": "https://json.schemastore.org/tsconfig",
  "display": "Default",
  "compilerOptions": {
    "strict": true,
    "strictNullChecks": true,
    "noUncheckedIndexedAccess": true,
    "esModuleInterop": true,
    "skipLibCheck": true,
    "moduleResolution": "bundler",
    "target": "ES2022",
    "lib": ["ES2022"],
    "module": "ESNext",
    "declaration": true,
    "declarationMap": true,
    "sourceMap": true
  }
}

nextjs.json:

{
  "$schema": "https://json.schemastore.org/tsconfig",
  "display": "Next.js",
  "extends": "./base.json",
  "compilerOptions": {
    "plugins": [{ "name": "next" }],
    "lib": ["dom", "dom.iterable", "esnext"],
    "module": "ESNext",
    "jsx": "preserve",
    "incremental": true
  },
  "exclude": ["node_modules"]
}

Use it in your Next.js app's tsconfig.json:

{
  "extends": "@repo/typescript-config/nextjs.json",
  "compilerOptions": {
    "baseUrl": ".",
    "paths": {
      "@/*": ["./src/*"]
    }
  },
  "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
  "exclude": ["node_modules"]
}

Shared UI components

packages/ui/package.json:

{
  "name": "@repo/ui",
  "version": "0.0.0",
  "private": true,
  "main": "./src/index.ts",
  "types": "./src/index.ts",
  "exports": {
    ".": "./src/index.ts"
  },
  "dependencies": {
    "react": "^18.0.0"
  },
  "devDependencies": {
    "@repo/typescript-config": "workspace:*",
    "typescript": "^5.0.0"
  }
}

Important: export from source (./src/index.ts), not from a build output. Next.js transpiles imported packages natively — no separate build step required for the UI package.

In your Next.js next.config.ts:

const nextConfig = {
  transpilePackages: ["@repo/ui"],
};

Root package.json scripts

{
  "scripts": {
    "build": "turbo run build",
    "dev": "turbo run dev",
    "lint": "turbo run lint",
    "type-check": "turbo run type-check",
    "test": "turbo run test",
    "clean": "turbo run clean && rm -rf node_modules"
  }
}

CI with remote caching

Turborepo's remote cache is the biggest velocity multiplier for teams. Without it, every CI run starts cold. With it, CI reuses cached task outputs from previous runs — locally or by other developers.

GitHub Actions with Vercel Remote Cache

Vercel provides remote caching for free. Get a token:

npx turbo login
npx turbo link

In your GitHub Actions workflow:

name: CI
 
on:
  push:
    branches: [main]
  pull_request:
 
jobs:
  build:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: pnpm/action-setup@v3
        with:
          version: 8
      - uses: actions/setup-node@v4
        with:
          node-version: 20
          cache: 'pnpm'
      - run: pnpm install --frozen-lockfile
      - run: pnpm build
        env:
          TURBO_TOKEN: ${{ secrets.TURBO_TOKEN }}
          TURBO_TEAM: ${{ vars.TURBO_TEAM }}
      - run: pnpm lint
        env:
          TURBO_TOKEN: ${{ secrets.TURBO_TOKEN }}
          TURBO_TEAM: ${{ vars.TURBO_TEAM }}
      - run: pnpm type-check
        env:
          TURBO_TOKEN: ${{ secrets.TURBO_TOKEN }}
          TURBO_TEAM: ${{ vars.TURBO_TEAM }}

Add TURBO_TOKEN and TURBO_TEAM to your GitHub repository secrets.

Common mistakes to avoid

Circular dependencies. apps/web importing from apps/docs is a circular dependency. Apps should only import from packages/. If two apps need the same code, it belongs in a package.

Building packages separately. If your UI package has its own build script that outputs to dist/, and your Next.js app imports from dist/, you need to rebuild the package every time you change it. Exporting from source and using transpilePackages avoids this entirely.

Not pinning Turborepo version. Turbo's caching is sensitive to version changes. Pin it: "turbo": "2.x.x" in your root devDependencies.

Missing outputs in turbo.json. If you don't declare outputs, Turbo can't restore them from cache. Every non-declared output re-runs even when inputs haven't changed.


This is the monorepo setup we use at Hunchbite across all our projects. The combination of Turborepo remote caching, shared TypeScript config, and source-exported packages eliminates the build overhead that slows most monorepos down.

If you're setting this up and running into specific issues, or if you want this kind of infrastructure installed across an existing codebase — get in touch.

FAQ
When should I use a monorepo instead of separate repositories?
A monorepo makes sense when two or more projects share code (UI components, utility functions, type definitions) and you want that shared code to be versioned together. The overhead of managing package versions across separate repos becomes painful fast. If your frontend and backend share TypeScript types, a monorepo eliminates the pain of keeping them in sync.
Is Turborepo better than Nx for Next.js projects?
For most Next.js projects, yes. Turborepo has a much simpler mental model — it's a build system, not a framework. The configuration is minimal, the caching just works, and it doesn't require learning a new layer of abstractions. Nx is more powerful but significantly more complex. Unless you have a very large monorepo with complex dependency graphs, Turborepo is the right choice.
How does Turborepo remote caching work?
Turborepo computes a hash of each task's inputs (source files, environment variables, dependencies). If a task with that exact hash has run before and its output is cached — locally or remotely — it uses the cache instead of running the task. In CI, this means a build that ran on one developer's PR doesn't re-run on another developer's PR if the inputs haven't changed. Significant CI time savings on large repos.
Next step

Ready to move forward?

If this guide resonated with your situation, let's talk. We offer a free 30-minute discovery call — no pitch, just honest advice on your specific project.

Book a Free CallSend a Message
Continue Reading
guide

Drizzle ORM Setup Guide: Type-Safe Database Access with PostgreSQL

How to set up Drizzle ORM with PostgreSQL from scratch — schema definition, migrations, query patterns, connection pooling, and the configuration decisions that matter in production Next.js applications.

11 min read
guide

How Database Indexes Work (And Why the Wrong Index Is Worse Than None)

A technical guide to database indexes: B-tree internals, composite index column ordering, covering indexes, partial indexes, the write cost of over-indexing, EXPLAIN ANALYZE interpretation, and the common indexing mistakes that degrade production performance.

14 min read
All Guides