Build a Monorepo with Next.js

A complete guide to setting up a production-ready monorepo with Turborepo, shared packages, and intelligent caching for faster builds.

What Is a Monorepo and Why Should You Care?

A monorepo is a version control strategy where multiple projects--such as separate applications, libraries, or services--live in a single repository. This approach contrasts with the traditional polyrepo model, where each project resides in its own repository. While polyrepos offer isolation, they often lead to challenges like duplicated code across projects, difficulty maintaining consistent dependencies and linting rules, and complex cross-project changes that require multiple pull requests and releases.

The monorepo approach addresses these issues by keeping everything in one place. When you modify a shared utility library, that change is immediately available to all applications that depend on it--no need to publish and update npm packages, manage version compatibility, or wait for CI/CD pipelines to propagate changes. Atomic commits allow you to change multiple related projects simultaneously, ensuring that breaking changes are caught immediately and that your entire codebase remains in a consistent state at all times.

The Turborepo Solution

Turborepo is a build system specifically designed for JavaScript and TypeScript monorepos. It solves the fundamental scaling problem that monorepos face: as your repository grows, naive build approaches become prohibitively slow because they rebuild everything from scratch every time. Turborepo uses intelligent caching to dramatically speed up builds by only rebuilding what has changed.

The system works by analyzing your task dependencies and running them in parallel wherever possible. When a task completes, Turborepo hashes the inputs (source files, dependencies, configuration) and caches the outputs. On subsequent runs, if the inputs haven't changed, Turborepo retrieves the cached outputs instantly instead of rerunning the task. This approach can reduce build times from hours to minutes in large monorepos.

Turborepo's Remote Cache takes this further by sharing build artifacts across your entire team and even across CI/CD pipelines. When one developer builds a task, the cache artifacts are uploaded to a remote storage location. Other developers--and your CI system--can then download these artifacts instead of rebuilding, ensuring that no one on your team ever does the same work twice.

Monorepo Benefits

Code Sharing

Share UI components, utilities, and types across all applications without npm package overhead.

Consistent Tooling

Unified ESLint, TypeScript, and build configurations across all projects in the repository.

Atomic Changes

Make coordinated changes across multiple projects in a single commit.

Intelligent Caching

Turborepo caches build outputs and only rebuilds what has changed.

Setting Up Your Monorepo Structure

A well-organized monorepo structure is essential for maintainability and scalability. The convention established by Turborepo and adopted by the community divides your repository into two primary directories: apps for deployable applications and packages for shared code that multiple applications can use.

The Apps Directory

The apps directory contains your frontend applications, backend services, and any other deployable units. Each subdirectory in apps represents a complete project that can be built and deployed independently. For example, you might have:

  • apps/web - A Next.js frontend
  • apps/api - An Express.js backend
  • apps/admin - A separate admin dashboard

The Packages Directory

The packages directory contains shared libraries that provide functionality across multiple applications. These packages are typically internal tools that aren't deployed on their own but are imported by applications in the apps directory:

  • packages/ui - Shared UI component library
  • packages/types - TypeScript types shared across projects
  • packages/config - ESLint and TypeScript configurations
  • packages/utils - Shared utility functions

Creating the Root Configuration

The root of your monorepo needs a package.json file that defines the workspace configuration and common scripts:

{
 "name": "my-monorepo",
 "private": true,
 "scripts": {
 "dev": "turbo run dev",
 "build": "turbo run build",
 "lint": "turbo run lint"
 },
 "workspaces": [
 "apps/*",
 "packages/*"
 ],
 "devDependencies": {
 "turbo": "latest"
 }
}

The workspaces field tells your package manager to treat all directories matching apps/* and packages/* as part of the workspace.

Installing and Configuring Turborepo

After setting up your workspace configuration, the next step is to configure Turborepo itself. Create a turbo.json file in the root of your repository:

{
 "$schema": "https://turborepo.com/schema.json",
 "tasks": {
 "build": {
 "dependsOn": ["^build"],
 "outputs": ["dist/**", ".next/**", "!.next/cache/**"]
 },
 "dev": {
 "cache": false,
 "persistent": true
 },
 "lint": {
 "dependsOn": ["^lint"]
 }
 }
}

Understanding Task Configuration

The dependsOn array specifies task dependencies. The ^ prefix means "wait for this task to complete in all dependencies before running." So dependsOn: ["^build"] in your Next.js application means "wait for all packages that this app depends on to build first."

The outputs array tells Turborepo which files to cache after a successful build. For a Next.js application, cache the .next directory but exclude .next/cache.

Creating a Next.js Application in Your Monorepo

With the foundation in place, you can now add a Next.js application to your monorepo. Navigate to the apps directory and create a new Next.js application:

cd apps
npx create-next-app@latest web --typescript --tailwind --eslint --app --src-dir --import-alias "@/*" --use-npm

This creates a fully configured Next.js application with TypeScript, Tailwind CSS, and ESLint.

Referencing Shared Packages

Update the application's package.json to reference shared packages using the workspace protocol:

{
 "name": "web",
 "dependencies": {
 "react": "^19.0.0",
 "react-dom": "^19.0.0",
 "next": "15.3.4",
 "@repo/ui": "workspace:*",
 "@repo/types": "workspace:*"
 }
}

The workspace:* protocol ensures that packages are resolved from your local packages/ directory rather than from npm. This approach provides consistency across all your applications while simplifying dependency management in your web development workflow.

Building Shared Packages for Code Reuse

One of the primary benefits of a monorepo is the ability to share code across multiple applications without the overhead of managing separate npm packages. By centralizing your shared logic, you ensure consistency across projects and reduce maintenance overhead.

Creating a UI Component Package

Create packages/ui with its own package.json:

{
 "name": "@repo/ui",
 "exports": {
 ".": "./src/index.ts",
 "./button": "./src/components/Button.tsx",
 "./card": "./src/components/Card.tsx"
 }
}

Creating a Shared Types Package

Create packages/types for centralized TypeScript definitions:

export interface User {
 id: string;
 email: string;
 name: string;
 role: 'admin' | 'user' | 'guest';
}

export interface ApiResponse<T> {
 data: T;
 status: number;
 message: string;
}

Applications can now import these types using import { User } from '@repo/types', ensuring consistency across all projects. This architectural approach aligns with modern AI automation workflows where type-safe APIs are essential for reliable integrations.

Best Practices for Monorepo Success

Dependency Management

Install common tooling (TypeScript, ESLint, Turborepo) as dev dependencies at the root, while application-specific dependencies live in their respective package.json files. Each application should declare its own dependencies, allowing Turborepo to properly cache and replay builds based on the dependency graph.

Caching Strategy

Configure the outputs array in turbo.json to be specific about what to cache. For Next.js applications, cache .next but exclude .next/cache. For shared packages, cache compiled output (dist/) but not source files.

Common Pitfalls

Circular Dependencies: Ensure your dependency graph has no cycles. Use npm ls to visualize the tree.

Version Mismatches: Use pnpm or yarn with strict hoisting behavior, or explicitly specify dependency versions.

Forgetting Remote Cache: Configure remote caching early to share build artifacts across your team.

Team Workflows

Encourage small, focused pull requests that affect a single package when possible. Configure CI/CD to take advantage of Turborepo's remote caching to reduce build times. This approach scales well whether you're building a small web application or a complex enterprise platform.

Frequently Asked Questions

What package managers support monorepo workspaces?

npm (v7+), pnpm, and yarn (v2+) all support workspace configurations. pnpm is often preferred for its strict hoisting behavior and efficient disk usage.

How do I add a new application to an existing monorepo?

Create the application in the apps/ directory, add it to your workspaces configuration, and ensure it has a package.json with the required scripts.

What is the difference between local and remote caching in Turborepo?

Local caching stores build artifacts on your machine. Remote caching shares these artifacts across your team and CI/CD pipelines, eliminating redundant builds.

Can I use Turborepo with non-Next.js projects?

Yes! Turborepo is tool-agnostic and works with any JavaScript/TypeScript project, including React, Vue, Express.js, NestJS, and more.

Conclusion

Building a monorepo with Next.js and Turborepo is a transformative step for development teams struggling with code duplication, slow builds, and complex dependency management. By keeping multiple applications and shared libraries in a single repository, you gain atomic cross-project changes, consistent tooling, and dramatically faster build times through intelligent caching.

Start simple--add a single Next.js application and one shared package to your monorepo. Establish the conventions you want to follow before the repository grows large. As your team and codebase expand, you'll find the monorepo structure scaling naturally to accommodate new projects while maintaining the productivity benefits that made you choose this approach in the first place.

The initial investment in setting up your monorepo pays dividends every day your team spends developing. Faster builds mean faster feedback loops. Shared packages reduce duplication and ensure consistency. And the ability to make atomic changes across projects eliminates the coordination overhead that plagues polyrepo architectures.

If you're looking to optimize your entire development workflow, consider partnering with experts who understand modern web development practices and can help you build scalable systems from day one.

Ready to Modernize Your Web Development Workflow?

Our team specializes in building scalable monorepo architectures and modern web applications. Let's discuss how we can help your team ship faster.