Next.js Monorepo with Turborepo: Enterprise Architecture
Set up a production monorepo for multiple Next.js applications using Turborepo.
20+ years shipping production JavaScript and front-end systems at scale. Written from production experience, not tutorials.
- ✓Node.js 18+, npm 9+ or pnpm 8+, Next.js 14+, Turborepo 1.10+, TypeScript 5+, familiarity with monorepo concepts, basic CI/CD knowledge (GitHub Actions), and a Vercel account for remote caching.
- Turborepo provides parallel task execution and remote caching for monorepo builds
- Structure:
apps/for Next.js applications,packages/for shared libraries - Use TypeScript project references for cross-package type checking
- Share UI components, validation schemas, and API clients across apps
- Turborepo's remote caching speeds up CI by 10x — never rebuild unchanged packages
- pnpm workspaces are the recommended package manager for Turborepo monorepos
A monorepo with Turborepo is like having multiple shops under one factory roof. Each shop (app) has its own assembly line, but they share the same tool storage (shared packages), quality control (linting/testing), and logistics (build pipeline). Turborepo is the factory foreman who makes sure nobody idles while waiting for shared parts.
| Chrome | Firefox | Safari | Edge |
|---|---|---|---|
| ✓ | ✓ | ✓ | ✓ |
Managing multiple Next.js applications — marketing site, dashboard, admin panel, API service — under a single repository requires deliberate architecture. Without structure, you get duplicated configurations, inconsistent dependency versions, and CI pipelines that rebuild everything for every change.
Turborepo solves these problems with parallel task execution, remote caching, and a dependency graph that knows exactly which packages need rebuilding. This guide covers the complete setup: project structure, shared packages, TypeScript configuration, CI/CD optimization, and deployment.
Why Monorepo? The Case for a Single Repository
In enterprise environments, managing multiple Next.js applications independently leads to duplicated code, inconsistent dependencies, and fragile cross-project coordination. A monorepo consolidates all projects into one repository, enabling shared linting, testing, and build configurations. Turborepo adds intelligent caching and parallel execution, reducing CI times from hours to minutes. The key trade-off is increased repository size and the need for disciplined code ownership boundaries. For teams with 3+ Next.js apps, the benefits far outweigh the costs.
Turborepo Architecture: Pipelines and Caching
Turborepo's core is the pipeline configuration in turbo.json. It defines task dependencies and caching rules. For example, build depends on ^build (upstream packages built first), and test depends on build. Turborepo caches outputs based on file contents, environment variables, and dependency graphs. Remote caching (via Vercel or S3) shares cache across CI runners, making cold builds as fast as incremental ones. Misconfigured pipelines (e.g., missing dependsOn) cause stale builds or unnecessary rebuilds.
turbo.json under globalDependencies or use --force to bypass cache.tsconfig.json in inputs. Always explicitly list all input globs.Structuring the Workspace: Apps and Packages
A typical enterprise monorepo has apps/ for Next.js applications and packages/ for shared libraries. Each app is a standalone Next.js project with its own next.config.js. Shared packages include UI components (@mycompany/ui), utilities (@mycompany/utils), and configuration (@mycompany/eslint-config). Use TypeScript project references for type checking across packages. Avoid circular dependencies by enforcing a strict dependency graph: packages can depend on other packages but not on apps.
@company/package-name) to avoid conflicts and clearly indicate ownership. This also works seamlessly with npm workspaces.shared/ folder inside each app. That led to drift and inconsistencies. Moving to a dedicated @mycompany/ui package with its own build pipeline fixed that, but we had to add a CI step to ensure the package is built before apps.Shared TypeScript Configuration
Consistency across the monorepo starts with a shared tsconfig.json base. Extend it in each app/package using extends. Use strict mode, enable composite for project references, and set declarationMap for better editor support. For Next.js apps, include jsx: "preserve" and moduleResolution: "bundler". For packages, use module: "commonjs" or "esnext" depending on your target. A common pitfall is forgetting to set rootDir and outDir correctly, causing type errors in consuming apps.
composite: true and a build script. Otherwise, tsc --build will fail silently.any types due to missing declaration: true. The consuming app didn't catch it because skipLibCheck was enabled. We now run tsc --noEmit on all packages in CI.Shared ESLint and Prettier Configuration
Enforce code style across the monorepo with a shared ESLint config that extends next/core-web-vitals and turbo. Use Prettier for formatting, integrated via eslint-config-prettier. For packages that don't use React, create a separate base config. Run linting in CI with turbo run lint to check all workspaces. A common mistake is having conflicting rules between the root config and workspace configs; use root: true in the root .eslintrc.js to prevent inheritance issues.
.prettierrc at the root with shared settings (e.g., singleQuote: true, trailingComma: 'all'). Run prettier --check . in CI to enforce formatting.console.log because the lint rule was only in the root config but not applied to packages. We now use overrides in ESLint to apply rules to all workspaces.Building Shared UI Components with Next.js
Shared UI components (buttons, modals, forms) should be built as a separate package using React and TypeScript. Export components as named exports from an index file. Use next/dynamic for code splitting in apps, but avoid it in the shared package to keep bundle optimization at the app level. For styling, use CSS Modules or Tailwind CSS with a shared preset. Ensure components are server-component compatible by marking them with 'use client' only when necessary.
'use client' at the top of the file. Otherwise, keep it as a server component for better performance.useState but wasn't marked as client component. Next.js 13 threw a cryptic error in production. We now enforce a lint rule that requires 'use client' for any file using React hooks.Managing Environment Variables Across Apps
Each Next.js app has its own .env.local for secrets and .env for defaults. For shared variables (e.g., API base URL), create a .env file at the root and reference it in each app's next.config.js using env option. Turborepo can pass environment variables via turbo.json's globalEnv to invalidate cache when they change. Never commit .env.local files; use .env.example as a template. A common failure is missing a variable in production, causing silent failures.
NEXT_PUBLIC_ if they need to be exposed to the browser. Secret variables (e.g., API keys) should only be used in server-side code or API routes..env file was missing a NEXT_PUBLIC_ prefix, causing the variable to be undefined in the browser. We now run a CI check that verifies all NEXT_PUBLIC_ variables are defined.Testing Strategy: Unit, Integration, and E2E
Use Jest for unit tests in packages and apps, with @testing-library/react for UI components. For integration tests across apps, use Playwright or Cypress. Run tests with turbo run test to leverage caching. Keep test files co-located with source files (e.g., Button.test.tsx). For E2E tests, create a separate package @mycompany/e2e that depends on all apps. A common pitfall is slow test suites due to unnecessary rebuilds; use Turborepo's inputs to only run tests on changed files.
test to your pipeline with inputs that only include test files and source files. This prevents re-running tests when only documentation changes.--runInBand in CI to avoid flakiness from parallel test execution.CI/CD Pipeline with Turborepo and GitHub Actions
Optimize CI by using Turborepo's remote caching. In GitHub Actions, set up a cache key based on turbo.json and lockfile. Use turbo run lint test build to run all checks. For deployment, deploy each app independently (e.g., to Vercel or AWS). Use turbo prune --scope=@mycompany/admin to create a minimal deployable artifact. A common mistake is not caching node_modules separately, causing long install times.
TURBO_TOKEN and TURBO_TEAM in your CI environment. This shares cache across all developers and CI runners.TURBO_TOKEN in CI once, and builds took 20 minutes instead of 2. Now we have a CI check that verifies remote caching is enabled.Handling Dependencies and Versioning
Use npm workspaces (or pnpm) for dependency management. Keep shared dependencies (React, Next.js) at the root package.json to ensure single version. For package-specific dependencies, declare them in the package's package.json. Use npm update carefully; prefer npm install package@version to avoid accidental upgrades. A common issue is hoisting conflicts where two packages require different versions of the same dependency; use overrides in root package.json to force a single version.
pnpm.overrides to force versions. For npm, check npm ls to see dependency tree and resolve conflicts.npm ls in CI to detect duplicate packages.Deploying Next.js Apps from Monorepo
Deploy each Next.js app independently. For Vercel, connect the monorepo and configure each app's root directory (e.g., apps/admin). Use turbo prune to generate a minimal deployment folder. For Docker, create a multi-stage build that copies only the pruned workspace. Ensure environment variables are set per app in the deployment platform. A common failure is deploying the entire monorepo, causing bloated images and slow cold starts.
apps/admin). Vercel automatically detects Next.js and uses its own build system, but you can override with a build command that runs turbo run build --filter=@mycompany/admin.turbo prune, the image size dropped to 200MB and cold starts improved by 80%.Monitoring and Debugging in Production
Use structured logging with pino or winston in API routes and server components. Aggregate logs using a service like Datadog or Grafana. For error tracking, integrate Sentry with each Next.js app. Turborepo doesn't affect runtime monitoring, but ensure each app has its own sentry.client.config.js and sentry.server.config.js. A common mistake is not distinguishing between apps in logs, making debugging cross-app issues difficult.
pino-pretty for local development and JSON output for production. Forward logs to a central service for querying.package field to all logs to trace the source.| File | Command / Code | Purpose |
|---|---|---|
| initialize-monorepo.sh | npx create-turbo@latest my-enterprise --example with-nextjs | Why Monorepo? The Case for a Single Repository |
| turbo.json | { | Turborepo Architecture |
| packages | { | Structuring the Workspace |
| packages | { | Shared TypeScript Configuration |
| packages | module.exports = { | Shared ESLint and Prettier Configuration |
| packages | interface ButtonProps extends React.ButtonHTMLAttributes | Building Shared UI Components with Next.js |
| apps | const path = require('path'); | Managing Environment Variables Across Apps |
| packages | describe('Button', () => { | Testing Strategy |
| .github | name: CI | CI/CD Pipeline with Turborepo and GitHub Actions |
| package.json (root) | { | Handling Dependencies and Versioning |
| apps | FROM node:20-alpine AS builder | Deploying Next.js Apps from Monorepo |
| apps | const logger = pino({ level: 'info', name: 'admin-api' }); | Monitoring and Debugging in Production |
Key takeaways
Frequently Asked Questions
20+ years shipping production JavaScript and front-end systems at scale. Written from production experience, not tutorials.
That's Next.js. Mark it forged?
3 min read · try the examples if you haven't