| name | monorepo-structure |
| description | Set up a Turborepo + pnpm monorepo for sharing code between frontend, backend, and workers. One repo, multiple packages, shared types, parallel builds. |
| license | MIT |
| compatibility | TypeScript/JavaScript |
| metadata | {"category":"foundations","time":"2h","source":"drift-masterguide"} |
Monorepo Structure
One repo, multiple packages, shared types, parallel builds.
When to Use This Skill
- Sharing code between frontend and backend
- Multiple apps need common types/utilities
- Want atomic commits across packages
- Tired of version hell with separate repos
- Need parallel builds with caching
Core Concepts
- Workspaces - pnpm manages multiple packages in one repo
- Turborepo - Orchestrates builds with caching and parallelization
- Shared types - Single source of truth for TypeScript types
- Build order - Dependencies build before dependents
Project Structure
project-root/
āāā apps/
ā āāā web/ # Next.js frontend
ā ā āāā app/
ā ā āāā components/
ā ā āāā package.json
ā āāā api/ # Backend API
ā ā āāā src/
ā ā āāā package.json
ā āāā worker/ # Background worker
ā āāā src/
ā āāā package.json
ā
āāā packages/
ā āāā types/ # Shared TypeScript types
ā ā āāā src/
ā ā ā āāā index.ts
ā ā ā āāā user.ts
ā ā ā āāā schemas.ts
ā ā āāā package.json
ā āāā utils/ # Shared utilities
ā ā āāā package.json
ā āāā config/ # Shared configs (eslint, tsconfig)
ā āāā package.json
ā
āāā package.json # Root package.json
āāā pnpm-workspace.yaml
āāā turbo.json
āāā tsconfig.base.json
TypeScript Implementation
pnpm-workspace.yaml
packages:
- "apps/*"
- "packages/*"
Root package.json
{
"name": "my-saas",
"private": true,
"scripts": {
"dev": "turbo dev",
"build": "turbo build",
"test": "turbo test",
"lint": "turbo lint",
"typecheck": "turbo typecheck",
"clean": "turbo clean && rm -rf node_modules"
},
"devDependencies": {
"turbo": "^2.0.0",
"typescript": "^5.4.0"
},
"packageManager": "pnpm@9.0.0"
}
turbo.json
{
"$schema": "https://turbo.build/schema.json",
"globalDependencies": ["**/.env.*local"],
"tasks": {
"build": {
"dependsOn": ["^build"],
"outputs": ["dist/**", ".next/**", "!.next/cache/**"]
},
"dev": {
"cache": false,
"persistent": true
},
"test": {
"dependsOn": ["^build"]
},
"typecheck": {
"dependsOn": ["^build"]
},
"lint": {
"dependsOn": ["^build"]
},
"clean": {
"cache": false
}
}
}
tsconfig.base.json
{
"compilerOptions": {
"target": "ES2022",
"lib": ["ES2022"],
"module": "ESNext",
"moduleResolution": "bundler",
"strict": true,
"noUncheckedIndexedAccess": true,
"noImplicitOverride": true,
"esModuleInterop": true,
"skipLibCheck": true,
"forceConsistentCasingInFileNames": true,
"declaration": true,
"declarationMap": true,
"sourceMap": true,
"resolveJsonModule": true,
"isolatedModules": true
}
}
Shared Types Package
{
"name": "@myapp/types",
"version": "0.0.1",
"private": true,
"main": "./dist/index.js",
"types": "./dist/index.d.ts",
"exports": {
".": {
"types": "./dist/index.d.ts",
"import": "./dist/index.js"
}
},
"scripts": {
"build": "tsc",
"dev": "tsc --watch",
"typecheck": "tsc --noEmit"
},
"devDependencies": {
"typescript": "^5.4.0"
},
"dependencies": {
"zod": "^3.23.0"
}
}
{
"extends": "../../tsconfig.base.json",
"compilerOptions": {
"outDir": "./dist",
"rootDir": "./src"
},
"include": ["src/**/*"]
}
export * from './user';
export * from './schemas';
export interface User {
id: string;
email: string;
name: string;
role: 'admin' | 'user' | 'guest';
createdAt: Date;
}
export interface CreateUserInput {
email: string;
name: string;
role?: 'admin' | 'user' | 'guest';
}
import { z } from 'zod';
export const UserSchema = z.object({
id: z.string().uuid(),
email: z.string().email(),
name: z.string().min(1),
role: z.enum(['admin', 'user', 'guest']),
createdAt: z.coerce.date(),
});
export const CreateUserSchema = z.object({
email: z.string().email(),
name: z.string().min(1),
role: z.enum(['admin', 'user', 'guest']).default('user'),
});
App Package Using Shared Types
{
"name": "@myapp/web",
"version": "0.0.1",
"private": true,
"scripts": {
"dev": "next dev",
"build": "next build",
"start": "next start"
},
"dependencies": {
"@myapp/types": "workspace:*",
"next": "^14.0.0",
"react": "^18.0.0"
}
}
import type { User, CreateUserInput } from '@myapp/types';
import { CreateUserSchema } from '@myapp/types';
export async function POST(request: Request) {
const body = await request.json();
const input = CreateUserSchema.parse(body);
const user: User = await createUser(input);
return Response.json(user);
}
Shared Utils Package
{
"name": "@myapp/utils",
"version": "0.0.1",
"private": true,
"main": "./dist/index.js",
"types": "./dist/index.d.ts",
"scripts": {
"build": "tsc",
"dev": "tsc --watch"
},
"devDependencies": {
"typescript": "^5.4.0"
}
}
export function formatDate(date: Date): string {
return date.toISOString().split('T')[0];
}
export function slugify(text: string): string {
return text
.toLowerCase()
.replace(/[^\w\s-]/g, '')
.replace(/\s+/g, '-');
}
export function sleep(ms: number): Promise<void> {
return new Promise(resolve => setTimeout(resolve, ms));
}
Common Commands
pnpm install
pnpm dev
pnpm build
pnpm test
pnpm add zod --filter @myapp/types
pnpm add -D prettier -w
pnpm --filter @myapp/web dev
pnpm --filter "@myapp/*" build
Dependency Flow
packages/types (source of truth)
ā
packages/utils (may import types)
ā
apps/web, apps/api, apps/worker (import both)
Turborepo handles build order via dependsOn: ["^build"] - packages always build before apps that depend on them.
.gitignore
# Dependencies
node_modules/
# Build outputs
dist/
.next/
.turbo/
# Environment
.env
.env.local
.env.*.local
# IDE
.idea/
.vscode/
# OS
.DS_Store
Best Practices
- Use
workspace:* - Always for internal dependencies
- Types flow down - Shared types package is the source of truth
- One tsconfig.base - Extend from root, override only what's needed
- Atomic commits - Change types and consumers in same commit
- Cache builds - Turborepo caches unchanged packages
Common Mistakes
- Using
^1.0.0 instead of workspace:* for internal deps
- Building packages individually instead of
turbo build
- Circular dependencies between packages
- Not including
dist/ in .gitignore
- Forgetting
dependsOn: ["^build"] in turbo.json
Related Skills