9.4 Monorepo Setup — npm workspaces and Turborepo
What is a Monorepo?
A monorepo manages multiple packages/apps in a single git repository.
my-monorepo/
├── apps/
│ ├── web/ # Next.js web app
│ └── mobile/ # React Native app
├── packages/
│ ├── ui/ # Shared UI components
│ ├── types/ # Shared type definitions
│ └── utils/ # Shared utility functions
├── package.json # Root (declares workspaces)
└── turbo.json # Turborepo configuration
Advantages:
- Easy code sharing
- Consistent development environment
- Atomic changes (commit multiple packages at once)
- Deduplicated dependencies
npm workspaces Setup
Root package.json
// package.json (root)
{
"name": "my-monorepo",
"private": true,
"workspaces": [
"apps/*",
"packages/*"
],
"scripts": {
"build": "turbo build",
"dev": "turbo dev",
"test": "turbo test",
"typecheck": "turbo typecheck"
},
"devDependencies": {
"turbo": "^2.0.0",
"typescript": "^5.4.0"
}
}
Installing Packages
# Install at root (available to all workspaces)
npm install --save-dev typescript -w
# Install in specific workspace
npm install react -w apps/web
npm install --save-dev @types/react -w apps/web
# Reference internal packages
npm install @myapp/ui -w apps/web
npm install @myapp/types -w packages/utils
Shared Types Package Structure
packages/types/
packages/types/
├── src/
│ ├── user.ts
│ ├── product.ts
│ ├── api.ts
│ └── index.ts
├── package.json
└── tsconfig.json
// packages/types/package.json
{
"name": "@myapp/types",
"version": "0.0.1",
"main": "./src/index.ts", // Direct TS source reference during dev
"types": "./src/index.ts",
"exports": {
".": {
"types": "./src/index.ts",
"default": "./src/index.ts"
}
}
}
// packages/types/src/user.ts
export interface User {
id: string;
name: string;
email: string;
role: 'admin' | 'user' | 'guest';
createdAt: Date;
}
export type CreateUserInput = Omit<User, 'id' | 'createdAt'>;
export type UpdateUserInput = Partial<CreateUserInput>;
// packages/types/src/api.ts
export interface ApiResponse<T = unknown> {
data: T;
status: number;
message: string;
}
export interface PaginatedResponse<T> {
items: T[];
total: number;
page: number;
limit: number;
hasNext: boolean;
}
export type ApiError = {
code: string;
message: string;
details?: Record<string, string[]>;
};
// packages/types/src/index.ts
export * from './user';
export * from './product';
export * from './api';
Shared UI Component Package
packages/ui/
// packages/ui/package.json
{
"name": "@myapp/ui",
"version": "0.0.1",
"main": "./src/index.ts",
"peerDependencies": {
"react": ">=18",
"react-dom": ">=18"
}
}
// packages/ui/src/Button.tsx
import React from 'react';
interface ButtonProps {
variant?: 'primary' | 'secondary' | 'ghost';
size?: 'sm' | 'md' | 'lg';
disabled?: boolean;
onClick?: () => void;
children: React.ReactNode;
}
export function Button({
variant = 'primary',
size = 'md',
disabled = false,
onClick,
children,
}: ButtonProps) {
return (
<button
className={`btn btn-${variant} btn-${size}`}
disabled={disabled}
onClick={onClick}
>
{children}
</button>
);
}
Shared tsconfig Setup
packages/tsconfig/ (Shared tsconfig)
// packages/tsconfig/base.json
{
"compilerOptions": {
"target": "ES2022",
"strict": true,
"esModuleInterop": true,
"skipLibCheck": true,
"forceConsistentCasingInFileNames": true,
"resolveJsonModule": true
}
}
// packages/tsconfig/react.json
{
"extends": "./base.json",
"compilerOptions": {
"module": "ESNext",
"moduleResolution": "bundler",
"jsx": "react-jsx",
"noEmit": true
}
}
// packages/tsconfig/node.json
{
"extends": "./base.json",
"compilerOptions": {
"module": "NodeNext",
"moduleResolution": "NodeNext"
}
}
Using Shared tsconfig in Apps
// apps/web/tsconfig.json
{
"extends": "@myapp/tsconfig/react.json",
"compilerOptions": {
"baseUrl": ".",
"paths": {
"@/*": ["./src/*"]
}
},
"include": ["src", "next-env.d.ts"]
}
Turborepo Configuration
Turborepo is a build caching and task orchestration tool for monorepos.
Installation
npm install --save-dev turbo
turbo.json
{
"$schema": "https://turbo.build/schema.json",
"tasks": {
"build": {
"dependsOn": ["^build"], // Build dependencies first
"outputs": ["dist/**", ".next/**"]
},
"dev": {
"cache": false, // Don't cache dev
"persistent": true // Keep running in background
},
"test": {
"dependsOn": ["build"],
"outputs": ["coverage/**"]
},
"typecheck": {
"dependsOn": ["^typecheck"]
},
"lint": {
"outputs": []
}
}
}
Turborepo Commands
# Full build (uses cache)
turbo build
# Build specific package only
turbo build --filter=@myapp/web
# Build only changed packages (based on git diff)
turbo build --filter=...[HEAD^1]
# Force build ignoring cache
turbo build --force
# Enable remote cache (Vercel)
turbo login
turbo link
TypeScript Project References + Monorepo
Use with Project References for more precise incremental builds.
// packages/types/tsconfig.json
{
"compilerOptions": {
"composite": true,
"declaration": true,
"outDir": "./dist",
"rootDir": "./src"
},
"include": ["src"]
}
// apps/web/tsconfig.json
{
"compilerOptions": { },
"references": [
{ "path": "../../packages/types" },
{ "path": "../../packages/ui" }
]
}
# Full build including project references
tsc --build apps/web
Real-World Monorepo Example
my-saas/
├── apps/
│ ├── web/ # Next.js app
│ │ ├── src/
│ │ ├── package.json
│ │ └── tsconfig.json
│ └── api/ # Express/NestJS API
│ ├── src/
│ ├── package.json
│ └── tsconfig.json
├── packages/
│ ├── database/ # Prisma schema + client
│ │ ├── prisma/schema.prisma
│ │ ├── src/
│ │ └── package.json
│ ├── types/ # Shared types
│ ├── ui/ # Shared UI components
│ ├── utils/ # Shared utilities
│ └── tsconfig/ # Shared tsconfig
├── package.json
└── turbo.json
// apps/web/src/app/users/page.tsx
import { UserCard } from '@myapp/ui';
import { formatDate } from '@myapp/utils';
import type { User } from '@myapp/types';
async function getUsers(): Promise<User[]> {
const res = await fetch('/api/users');
return res.json();
}
export default async function UsersPage() {
const users = await getUsers();
return (
<div>
{users.map(user => (
<UserCard
key={user.id}
user={user}
subtitle={formatDate(user.createdAt)}
/>
))}
</div>
);
}
Pro Tips
1. Package versioning strategy
Independent versioning: Each package has its own version (use changesets)
Fixed versioning: All packages share the same version (Lerna fixed mode)
2. Release management with changesets
npm install --save-dev @changesets/cli
npx changeset init
# Record changes
npx changeset add
# Update versions + generate CHANGELOG
npx changeset version
# Publish
npx changeset publish
3. Internal package builds vs direct source reference
// During development: direct TS source (reflects changes without rebuilding)
{ "main": "./src/index.ts" }
// For publishing: built JS reference
{ "main": "./dist/index.js", "types": "./dist/index.d.ts" }