A monorepo with Turborepo, Next.js, and NestJS is the architecture I reach for on any project where the frontend and backend are built by the same team. Shared TypeScript types across the boundary eliminate the most common source of full-stack bugs — API shape mismatches. Shared ESLint configs and TypeScript configs mean one team-wide configuration. Turborepo's intelligent caching means builds only rerun what changed. I've set this up for multiple commercial projects and it's the architecture I recommend for any team of 2-10.
You could manage a monorepo with just pnpm workspaces or Yarn workspaces — that gives you package hoisting and workspace references. Turborepo adds on top: task pipelining (run test only after build completes), remote caching (share build artifacts across team members and CI), and parallel execution across packages. In practice, the biggest win for most teams is the pipeline configuration: turbo build runs the build for all packages in the correct order, caching results. A full repository build that takes 4 minutes on first run takes 15 seconds on second run if nothing changed.
The standard Turborepo structure has three top-level directories: apps/ (runnable applications — your Next.js frontend, your NestJS backend), packages/ (shared libraries — types, UI components, config), and the root turbo.json. The apps/web Next.js app and apps/api NestJS app both depend on packages/types, which exports all shared TypeScript interfaces and Zod schemas. Both apps also depend on packages/eslint-config and packages/tsconfig for unified configuration.
my-project/ (Turborepo root)
├── turbo.json ← pipeline configuration
├── pnpm-workspace.yaml
├── package.json ← root scripts
│
├── apps/
│ ├── web/ ← Next.js frontend
│ │ ├── package.json depends on @myproject/types, @myproject/ui
│ │ └── src/
│ │ └── app/ ← Next.js App Router
│ └── api/ ← NestJS backend
│ ├── package.json depends on @myproject/types
│ └── src/
│ └── ...
│
└── packages/
├── types/ ← shared TypeScript types + Zod schemas
│ ├── package.json
│ └── src/
│ ├── user.ts export interface User {...}
│ ├── order.ts export const orderSchema = z.object({...})
│ └── index.ts export * from './user'; export * from './order'
├── ui/ ← shared React components (optional)
│ └── src/
│ └── Button.tsx
├── eslint-config/ ← shared ESLint config
└── tsconfig/ ← shared TypeScript configs
├── base.json
├── nextjs.json
└── nestjs.json
Type flow (full-stack type safety):
packages/types/src/order.ts
│ export interface CreateOrderDto
│ export const createOrderSchema = z.object(...)
├──► apps/api/ NestJS controller return type + Zod validation pipe
└──► apps/web/ Next.js fetch() response type + react-hook-form zodResolverFrom setting up a Turborepo monorepo for a commercial project at Commsult: put your Zod schemas in the shared packages/types package, not just TypeScript interfaces. Zod schemas give you runtime validation AND TypeScript inference from a single source. Your NestJS API uses the Zod schema for input validation (via a custom Zod pipe), and your Next.js frontend uses the same schema for form validation (via react-hook-form's zodResolver). One schema, three uses: type definition, API validation, and form validation.
Bootstrap with: pnpm dlx create-turbo@latest. Choose the pnpm workspace example and select the packages you need. The generated turbo.json defines your task pipeline. The critical configuration: add 'dependsOn: ["^build"]' to your build task so dependent packages build before the consuming apps. For development, run turbo dev to start all apps in watch mode simultaneously — Next.js on port 3000, NestJS on port 3001, with hot module reloading in both.
# Bootstrap (choose pnpm + with-nestjs example or blank)
pnpm dlx create-turbo@latest
# turbo.json — pipeline configuration
{
"$schema": "https://turbo.build/schema.json",
"tasks": {
"build": {
"dependsOn": ["^build"], // build dependencies before consumers
"outputs": [".next/**", "!.next/cache/**", "dist/**"]
},
"dev": {
"cache": false,
"persistent": true // keep running (watch mode)
},
"test": {
"dependsOn": ["^build"],
"outputs": ["coverage/**"]
},
"lint": {
"outputs": []
}
}
}
# packages/types/package.json
{
"name": "@myproject/types",
"main": "./dist/index.js",
"types": "./dist/index.d.ts",
"scripts": {
"build": "tsc",
"dev": "tsc --watch"
}
}
# apps/web/package.json — consume shared types
{
"dependencies": {
"@myproject/types": "workspace:*"
}
}
# pnpm-workspace.yaml
packages:
- 'apps/*'
- 'packages/*'
# Run everything in parallel with Turborepo
pnpm turbo dev # starts Next.js (3000) + NestJS (3001) simultaneously
pnpm turbo build # builds all packages in correct dependency order
pnpm turbo test # runs tests only for changed packages
# Remote cache (connect to Vercel or self-host)
# .env
TURBO_TOKEN=your-token
TURBO_TEAM=your-team-idThe packages/types package is the most valuable piece. It exports shared interfaces: API request/response types, DTOs that match your Prisma models, and shared enums. The NestJS backend imports these types for its controller return types and DTO validation. The Next.js frontend imports them for fetch() response typing and form schemas. When you change an API response shape, TypeScript immediately shows errors in both the NestJS return statement AND the Next.js component consuming the response — full-stack type safety.
The most common Turborepo gotcha: circular dependencies between packages. If packages/ui imports from packages/types, packages/types must NOT import from packages/ui. Maintain a strict dependency hierarchy: apps/ depends on packages/, packages/ may depend on other packages/ but never on apps/. Use madge to audit your dependency graph before it grows complex: npx madge --circular --extensions ts ./packages. Circular dependencies cause cryptic build failures and can be extremely difficult to untangle once established.
Turborepo's remote cache is its biggest CI/CD advantage. After the first CI run, subsequent runs skip tasks whose inputs haven't changed. A PR that only changes the Next.js app won't rebuild the NestJS app. Connect to Vercel's remote cache (free for Vercel users) or self-host with turbo-remote-cache on your infrastructure. Add TURBO_TOKEN and TURBO_TEAM to your CI environment variables. Build times in GitHub Actions drop from 8 minutes to 2 minutes for most PRs once the cache is warm.
Turborepo + pnpm workspaces is my default for any project where Next.js and NestJS share a codebase. The shared types package alone justifies the setup — catching API shape mismatches at compile time has prevented multiple production incidents. The Turborepo remote cache is genuinely impressive in CI: a warm cache makes PRs merge in minutes rather than 10+ minutes. The initial setup takes half a day; the productivity return over a 6-month project is substantial.
Nx is Turborepo's main competitor for monorepo tooling. Nx is more opinionated — it generates code, enforces module boundaries, and has a richer plugin ecosystem. Turborepo is simpler — it orchestrates existing tooling without generating code or enforcing boundaries. For a small team already using Next.js and NestJS with their own tooling choices, Turborepo's simplicity is an advantage. For a large team that wants enforced architecture boundaries and more generator tooling, Nx scales better. Both are production-ready — the choice is team preference.