Blog/Tutorials & Step-by-Step/Set Up a Monorepo with Turborepo, Next.js, and NestJS
POST
December 08, 2025
LAST UPDATEDDecember 08, 2025

Set Up a Monorepo with Turborepo, Next.js, and NestJS

Complete guide to setting up a Turborepo monorepo with Next.js frontend and NestJS backend, including shared packages, TypeScript configs, and CI/CD pipelines.

Tags

TurborepoMonorepoNext.jsNestJS
Set Up a Monorepo with Turborepo, Next.js, and NestJS
4 min read

Set Up a Monorepo with Turborepo, Next.js, and NestJS

TL;DR

Turborepo's task caching and dependency graph make it possible to run a Next.js frontend and NestJS backend in a single monorepo with shared TypeScript packages, cutting CI build times significantly through incremental builds and remote caching.

Prerequisites

  • Node.js 18+ and pnpm 8+ installed
  • Basic familiarity with Next.js and NestJS
  • A GitHub account (for remote caching setup)
  • Terminal/command line experience

Step 1: Initialize the Turborepo

Start by scaffolding a new Turborepo workspace. We use pnpm because it handles monorepo dependencies efficiently with its content-addressable storage.

bash
npx create-turbo@latest my-monorepo
cd my-monorepo

When prompted, select pnpm as the package manager. The scaffolded project includes an apps/ and packages/ directory. Remove the default apps since we will create our own.

bash
rm -rf apps/web apps/docs

Your root package.json should declare the workspace structure:

json
{
  "name": "my-monorepo",
  "private": true,
  "scripts": {
    "build": "turbo build",
    "dev": "turbo dev",
    "lint": "turbo lint",
    "type-check": "turbo type-check"
  },
  "devDependencies": {
    "turbo": "^2.0.0"
  },
  "packageManager": "pnpm@8.15.0"
}

And your pnpm-workspace.yaml:

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

Step 2: Create the Next.js Frontend

Create the Next.js application inside the apps/ directory.

bash
cd apps
npx create-next-app@latest web --typescript --tailwind --app --src-dir --no-import-alias
cd ..

Update apps/web/package.json to set the name and add workspace scripts:

json
{
  "name": "@repo/web",
  "version": "0.0.0",
  "private": true,
  "scripts": {
    "dev": "next dev --port 3000",
    "build": "next build",
    "start": "next start",
    "lint": "next lint",
    "type-check": "tsc --noEmit"
  }
}

Step 3: Create the NestJS Backend

Now scaffold the NestJS API inside apps/.

bash
cd apps
npx @nestjs/cli new api --package-manager pnpm --skip-git
cd ..

Update apps/api/package.json:

json
{
  "name": "@repo/api",
  "version": "0.0.0",
  "private": true,
  "scripts": {
    "dev": "nest start --watch",
    "build": "nest build",
    "start": "node dist/main",
    "lint": "eslint \"{src,apps,libs,test}/**/*.ts\"",
    "type-check": "tsc --noEmit"
  }
}

Verifying the Structure

At this point, your directory should look like this:

my-monorepo/
├── apps/
│   ├── web/          # Next.js frontend
│   └── api/          # NestJS backend
├── packages/         # Shared packages (empty for now)
├── turbo.json
├── pnpm-workspace.yaml
└── package.json

Step 4: Create the Shared Types Package

This is where the monorepo shines. Create a shared package for types, DTOs, and validation schemas that both apps consume.

bash
mkdir -p packages/shared-types/src

Create packages/shared-types/package.json:

json
{
  "name": "@repo/shared-types",
  "version": "0.0.0",
  "private": true,
  "main": "./src/index.ts",
  "types": "./src/index.ts",
  "scripts": {
    "type-check": "tsc --noEmit",
    "lint": "eslint src/"
  },
  "devDependencies": {
    "typescript": "^5.3.0"
  }
}

Create packages/shared-types/tsconfig.json:

json
{
  "extends": "../../tsconfig.base.json",
  "compilerOptions": {
    "outDir": "./dist",
    "rootDir": "./src"
  },
  "include": ["src"]
}

Now add your shared types:

typescript
// packages/shared-types/src/index.ts
export interface User {
  id: string;
  email: string;
  name: string;
  role: "admin" | "user";
  createdAt: string;
}
 
export interface ApiResponse<T> {
  data: T;
  message: string;
  statusCode: number;
}
 
export interface PaginatedResponse<T> extends ApiResponse<T[]> {
  total: number;
  page: number;
  limit: number;
  hasMore: boolean;
}
 
export interface CreateUserDto {
  email: string;
  name: string;
  password: string;
}
 
export interface LoginDto {
  email: string;
  password: string;
}

Consuming Shared Types

Add the shared package as a dependency in both apps:

bash
# In apps/web/package.json, add:
pnpm --filter @repo/web add @repo/shared-types --workspace
 
# In apps/api/package.json, add:
pnpm --filter @repo/api add @repo/shared-types --workspace

Now both apps can import from the same source:

typescript
// In apps/web/src/app/page.tsx
import type { User, ApiResponse } from "@repo/shared-types";
 
async function getUsers(): Promise<ApiResponse<User[]>> {
  const res = await fetch("http://localhost:4000/users");
  return res.json();
}
typescript
// In apps/api/src/users/users.controller.ts
import { Controller, Get } from "@nestjs/common";
import type { User, ApiResponse } from "@repo/shared-types";
 
@Controller("users")
export class UsersController {
  @Get()
  findAll(): ApiResponse<User[]> {
    return {
      data: [],
      message: "Users retrieved",
      statusCode: 200,
    };
  }
}

Step 5: Set Up Shared TypeScript Configuration

Create a base TypeScript config that all packages extend.

json
// tsconfig.base.json (root)
{
  "compilerOptions": {
    "target": "ES2022",
    "lib": ["ES2022"],
    "module": "ESNext",
    "moduleResolution": "bundler",
    "strict": true,
    "esModuleInterop": true,
    "skipLibCheck": true,
    "forceConsistentCasingInFileNames": true,
    "resolveJsonModule": true,
    "isolatedModules": true,
    "declaration": true,
    "declarationMap": true,
    "sourceMap": true
  },
  "exclude": ["node_modules", "dist"]
}

Each app extends this base config. For example, apps/web/tsconfig.json:

json
{
  "extends": "../../tsconfig.base.json",
  "compilerOptions": {
    "jsx": "preserve",
    "module": "ESNext",
    "moduleResolution": "bundler",
    "plugins": [{ "name": "next" }],
    "paths": {
      "@/*": ["./src/*"]
    }
  },
  "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx"],
  "exclude": ["node_modules"]
}

Step 6: Configure Turborepo Pipelines

The turbo.json file defines how tasks run and their dependency relationships.

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

Understanding the Pipeline

  • "dependsOn": ["^build"] means a package's build runs only after all of its workspace dependencies finish building. The ^ prefix indicates topological ordering.
  • "outputs" tells Turborepo which files to cache. When inputs have not changed, these outputs are restored from cache instead of rebuilding.
  • "persistent": true for dev keeps the task running (watch mode) and disables caching since dev servers should not be cached.

Step 7: Add Development Scripts

Update the root package.json with useful development scripts:

json
{
  "scripts": {
    "dev": "turbo dev",
    "dev:web": "turbo dev --filter=@repo/web",
    "dev:api": "turbo dev --filter=@repo/api",
    "build": "turbo build",
    "lint": "turbo lint",
    "type-check": "turbo type-check",
    "clean": "turbo clean && rm -rf node_modules"
  }
}

Run pnpm dev to start both apps in parallel. Turborepo knows the dependency graph, so it starts shared-types first (if it had a build step), then launches both web and api concurrently.

bash
pnpm dev
# Starts Next.js on port 3000 and NestJS on port 3000 (NestJS default)

Step 8: Enable Remote Caching

Remote caching shares build artifacts across your team and CI. Connect to Vercel's remote cache:

bash
npx turbo login
npx turbo link

Once linked, any build artifacts cached locally are pushed to the remote cache. When a teammate or CI runner builds the same unchanged package, it pulls from the cache instead of rebuilding.

For CI pipelines, set the TURBO_TOKEN and TURBO_TEAM environment variables:

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

Putting It All Together

Your monorepo now has a clear architecture:

  • apps/web -- Next.js frontend consuming shared types for API contracts
  • apps/api -- NestJS backend using the same shared types for request/response validation
  • packages/shared-types -- single source of truth for DTOs, interfaces, and contracts
  • turbo.json -- orchestrates builds with dependency awareness and caching

Running pnpm build builds everything in the correct order. Running pnpm dev starts both apps in parallel. Changing a shared type triggers rebuilds only in the packages that depend on it.

Next Steps

  • Add a shared validation package -- use Zod schemas in packages/shared-validation that both apps reference for runtime validation
  • Set up a shared UI package -- create packages/ui for React components shared across multiple frontend apps
  • Add database packages -- put Prisma schemas in a shared package so migrations are centralized
  • Configure environment variables -- use dotenv-cli with per-app .env files and Turborepo's globalDependencies
  • Add E2E tests -- create a packages/e2e workspace that runs Playwright tests against both apps

FAQ

Why use Turborepo instead of Nx for a monorepo?

Turborepo is simpler to configure and integrates seamlessly with the npm/pnpm workspace ecosystem. It excels at incremental builds and remote caching with minimal configuration, making it ideal for teams that want monorepo benefits without a steep learning curve.

How do you share TypeScript types between Next.js and NestJS?

Create a shared packages directory (e.g., packages/shared-types) with its own tsconfig and package.json. Both apps reference it as a workspace dependency, giving you a single source of truth for DTOs, API contracts, and validation schemas.

Does Turborepo support remote caching?

Yes. Turborepo supports remote caching through Vercel or self-hosted solutions. Once enabled, build artifacts are shared across team members and CI runs, so unchanged packages are never rebuilt twice.

Collaboration

Need help with a project?

Let's Build It

I help startups and established companies design, build, and scale world-class digital products. From deep technical architecture to pixel-perfect UI — let's bring your vision to life.

SH

Article Author

Sadam Hussain

Senior Full Stack Developer

Senior Full Stack Developer with over 7 years of experience building React, Next.js, Node.js, TypeScript, and AI-powered web platforms.

Related Articles

How to Add Observability to a Node.js App with OpenTelemetry
Mar 21, 20265 min read
Node.js
OpenTelemetry
Observability

How to Add Observability to a Node.js App with OpenTelemetry

Learn how to instrument a Node.js app with OpenTelemetry for traces, metrics, and logs, and build a practical observability setup for production debugging.

How to Build a Backend-for-Frontend (BFF) with Next.js and Node.js
Mar 21, 20266 min read
Next.js
Node.js
BFF

How to Build a Backend-for-Frontend (BFF) with Next.js and Node.js

A practical guide to building a Backend-for-Frontend with Next.js and Node.js for API aggregation, auth handling, caching, and frontend-specific data shaping.

How I Structure CI/CD for Next.js, Docker, and GitHub Actions
Mar 21, 20265 min read
CI/CD
Next.js
Docker

How I Structure CI/CD for Next.js, Docker, and GitHub Actions

A practical CI/CD blueprint for Next.js apps using Docker and GitHub Actions, including testing, image builds, deployment stages, cache strategy, and release safety.