monorepo-setup
Monorepo Setup
Configure a scalable monorepo with Turborepo and pnpm workspaces.
Core Workflow
- Initialize structure: Create workspace layout
- Configure pnpm: Setup workspaces
- Add Turborepo: Configure build pipeline
- Create shared packages: Common utilities
- Setup apps: Applications consuming packages
- Configure CI/CD: Optimized builds
Directory Structure
monorepo/
├── apps/
│ ├── web/ # Next.js app
│ ├── api/ # Express/Fastify API
│ └── mobile/ # React Native app
├── packages/
│ ├── ui/ # Shared UI components
│ ├── config/ # Shared configs
│ ├── tsconfig/ # TypeScript configs
│ ├── eslint-config/ # ESLint configs
│ └── utils/ # Shared utilities
├── tooling/
│ ├── scripts/ # Build scripts
│ └── docker/ # Docker configs
├── turbo.json
├── pnpm-workspace.yaml
├── package.json
└── .npmrc
Root Configuration
pnpm Workspace
# pnpm-workspace.yaml
packages:
- "apps/*"
- "packages/*"
- "tooling/*"
Root Package.json
{
"name": "monorepo",
"private": true,
"packageManager": "pnpm@8.15.0",
"engines": {
"node": ">=20.0.0",
"pnpm": ">=8.0.0"
},
"scripts": {
"build": "turbo run build",
"dev": "turbo run dev",
"lint": "turbo run lint",
"test": "turbo run test",
"typecheck": "turbo run typecheck",
"clean": "turbo run clean && rm -rf node_modules",
"format": "prettier --write \"**/*.{ts,tsx,md,json}\"",
"changeset": "changeset",
"version-packages": "changeset version",
"release": "turbo run build --filter=./packages/* && changeset publish"
},
"devDependencies": {
"@changesets/cli": "^2.27.0",
"prettier": "^3.2.0",
"turbo": "^2.0.0",
"typescript": "^5.3.0"
}
}
NPM Configuration
# .npmrc
auto-install-peers=true
strict-peer-dependencies=false
shamefully-hoist=true
node-linker=hoisted
# Private registry (optional)
# @company:registry=https://npm.company.com/
# //npm.company.com/:_authToken=${NPM_TOKEN}
Turborepo Configuration
// turbo.json
{
"$schema": "https://turbo.build/schema.json",
"globalDependencies": ["**/.env.*local"],
"globalEnv": ["NODE_ENV", "CI"],
"pipeline": {
"build": {
"dependsOn": ["^build"],
"outputs": ["dist/**", ".next/**", "!.next/cache/**"],
"env": ["DATABASE_URL", "API_URL"]
},
"dev": {
"dependsOn": ["^build"],
"cache": false,
"persistent": true
},
"lint": {
"dependsOn": ["^build"],
"outputs": []
},
"typecheck": {
"dependsOn": ["^build"],
"outputs": []
},
"test": {
"dependsOn": ["build"],
"outputs": ["coverage/**"]
},
"clean": {
"cache": false
}
}
}
Shared TypeScript Config
// packages/tsconfig/base.json
{
"$schema": "https://json.schemastore.org/tsconfig",
"display": "Default",
"compilerOptions": {
"composite": false,
"declaration": true,
"declarationMap": true,
"esModuleInterop": true,
"forceConsistentCasingInFileNames": true,
"inlineSources": false,
"isolatedModules": true,
"moduleResolution": "bundler",
"noUnusedLocals": false,
"noUnusedParameters": false,
"preserveWatchOutput": true,
"skipLibCheck": true,
"strict": true,
"strictNullChecks": true
},
"exclude": ["node_modules"]
}
// packages/tsconfig/nextjs.json
{
"$schema": "https://json.schemastore.org/tsconfig",
"display": "Next.js",
"extends": "./base.json",
"compilerOptions": {
"lib": ["dom", "dom.iterable", "ES2022"],
"module": "ESNext",
"target": "ES2022",
"jsx": "preserve",
"noEmit": true,
"plugins": [{ "name": "next" }],
"allowJs": true,
"incremental": true,
"resolveJsonModule": true
}
}
// packages/tsconfig/library.json
{
"$schema": "https://json.schemastore.org/tsconfig",
"display": "Library",
"extends": "./base.json",
"compilerOptions": {
"lib": ["ES2022"],
"module": "ESNext",
"target": "ES2022",
"outDir": "dist",
"rootDir": "src"
}
}
Shared UI Package
// packages/ui/package.json
{
"name": "@repo/ui",
"version": "0.0.0",
"private": true,
"exports": {
".": "./src/index.ts",
"./button": "./src/button.tsx",
"./card": "./src/card.tsx",
"./styles.css": "./src/styles.css"
},
"scripts": {
"build": "tsup",
"dev": "tsup --watch",
"lint": "eslint src/",
"typecheck": "tsc --noEmit",
"clean": "rm -rf dist"
},
"peerDependencies": {
"react": "^18.0.0",
"react-dom": "^18.0.0"
},
"devDependencies": {
"@repo/eslint-config": "workspace:*",
"@repo/tsconfig": "workspace:*",
"@types/react": "^18.2.0",
"react": "^18.2.0",
"tsup": "^8.0.0",
"typescript": "^5.3.0"
}
}
// packages/ui/src/button.tsx
import { ButtonHTMLAttributes, forwardRef } from 'react';
export interface ButtonProps extends ButtonHTMLAttributes<HTMLButtonElement> {
variant?: 'primary' | 'secondary' | 'outline';
size?: 'sm' | 'md' | 'lg';
}
export const Button = forwardRef<HTMLButtonElement, ButtonProps>(
({ variant = 'primary', size = 'md', className = '', ...props }, ref) => {
const baseStyles = 'inline-flex items-center justify-center rounded-md font-medium transition-colors';
const variants = {
primary: 'bg-blue-600 text-white hover:bg-blue-700',
secondary: 'bg-gray-200 text-gray-900 hover:bg-gray-300',
outline: 'border border-gray-300 bg-transparent hover:bg-gray-100',
};
const sizes = {
sm: 'h-8 px-3 text-sm',
md: 'h-10 px-4',
lg: 'h-12 px-6 text-lg',
};
return (
<button
ref={ref}
className={`${baseStyles} ${variants[variant]} ${sizes[size]} ${className}`}
{...props}
/>
);
}
);
Button.displayName = 'Button';
// packages/ui/src/index.ts
export { Button, type ButtonProps } from './button';
export { Card, type CardProps } from './card';
// packages/ui/tsup.config.ts
import { defineConfig } from 'tsup';
export default defineConfig({
entry: ['src/index.ts'],
format: ['esm', 'cjs'],
dts: true,
splitting: false,
sourcemap: true,
clean: true,
external: ['react', 'react-dom'],
});
Shared Utils Package
// packages/utils/package.json
{
"name": "@repo/utils",
"version": "0.0.0",
"private": true,
"main": "./dist/index.js",
"module": "./dist/index.mjs",
"types": "./dist/index.d.ts",
"exports": {
".": {
"import": "./dist/index.mjs",
"require": "./dist/index.js",
"types": "./dist/index.d.ts"
}
},
"scripts": {
"build": "tsup",
"dev": "tsup --watch",
"test": "vitest run",
"test:watch": "vitest",
"typecheck": "tsc --noEmit",
"clean": "rm -rf dist"
},
"devDependencies": {
"@repo/tsconfig": "workspace:*",
"tsup": "^8.0.0",
"typescript": "^5.3.0",
"vitest": "^1.2.0"
}
}
// packages/utils/src/index.ts
export { cn } from './cn';
export { formatDate, formatRelativeTime } from './date';
export { debounce, throttle } from './timing';
export { sleep, retry } from './async';
// packages/utils/src/cn.ts
import { type ClassValue, clsx } from 'clsx';
import { twMerge } from 'tailwind-merge';
export function cn(...inputs: ClassValue[]): string {
return twMerge(clsx(inputs));
}
Next.js App Configuration
// apps/web/package.json
{
"name": "@repo/web",
"version": "0.1.0",
"private": true,
"scripts": {
"dev": "next dev",
"build": "next build",
"start": "next start",
"lint": "next lint",
"typecheck": "tsc --noEmit",
"clean": "rm -rf .next .turbo"
},
"dependencies": {
"@repo/ui": "workspace:*",
"@repo/utils": "workspace:*",
"next": "^14.1.0",
"react": "^18.2.0",
"react-dom": "^18.2.0"
},
"devDependencies": {
"@repo/eslint-config": "workspace:*",
"@repo/tsconfig": "workspace:*",
"@types/node": "^20.11.0",
"@types/react": "^18.2.0",
"@types/react-dom": "^18.2.0",
"typescript": "^5.3.0"
}
}
// apps/web/tsconfig.json
{
"extends": "@repo/tsconfig/nextjs.json",
"compilerOptions": {
"baseUrl": ".",
"paths": {
"@/*": ["./src/*"]
}
},
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
"exclude": ["node_modules"]
}
// apps/web/src/app/page.tsx
import { Button } from '@repo/ui/button';
import { formatDate } from '@repo/utils';
export default function Home() {
return (
<main className="flex min-h-screen flex-col items-center justify-center">
<h1 className="text-4xl font-bold">Monorepo App</h1>
<p className="mt-4">Today is {formatDate(new Date())}</p>
<Button className="mt-4">Click me</Button>
</main>
);
}
ESLint Config Package
// packages/eslint-config/package.json
{
"name": "@repo/eslint-config",
"version": "0.0.0",
"private": true,
"exports": {
"./base": "./base.js",
"./next": "./next.js",
"./react": "./react.js",
"./library": "./library.js"
},
"devDependencies": {
"@typescript-eslint/eslint-plugin": "^7.0.0",
"@typescript-eslint/parser": "^7.0.0",
"eslint": "^8.56.0",
"eslint-config-next": "^14.1.0",
"eslint-config-prettier": "^9.1.0",
"eslint-plugin-react": "^7.33.0",
"eslint-plugin-react-hooks": "^4.6.0"
}
}
// packages/eslint-config/base.js
module.exports = {
parser: '@typescript-eslint/parser',
extends: [
'eslint:recommended',
'plugin:@typescript-eslint/recommended',
'prettier',
],
plugins: ['@typescript-eslint'],
env: {
node: true,
es2022: true,
},
rules: {
'@typescript-eslint/no-unused-vars': ['error', { argsIgnorePattern: '^_' }],
'@typescript-eslint/no-explicit-any': 'warn',
},
ignorePatterns: ['dist/', 'node_modules/', '.turbo/'],
};
// packages/eslint-config/next.js
module.exports = {
extends: [
'./base.js',
'next/core-web-vitals',
'plugin:react-hooks/recommended',
],
rules: {
'react/react-in-jsx-scope': 'off',
},
};
CI/CD Configuration
# .github/workflows/ci.yml
name: CI
on:
push:
branches: [main]
pull_request:
branches: [main]
env:
TURBO_TOKEN: ${{ secrets.TURBO_TOKEN }}
TURBO_TEAM: ${{ vars.TURBO_TEAM }}
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 2
- uses: pnpm/action-setup@v3
with:
version: 8
- uses: actions/setup-node@v4
with:
node-version: 20
cache: 'pnpm'
- name: Install dependencies
run: pnpm install --frozen-lockfile
- name: Build
run: pnpm build
- name: Lint
run: pnpm lint
- name: Type check
run: pnpm typecheck
- name: Test
run: pnpm test
Changeset Configuration
// .changeset/config.json
{
"$schema": "https://unpkg.com/@changesets/config@3.0.0/schema.json",
"changelog": "@changesets/cli/changelog",
"commit": false,
"fixed": [],
"linked": [],
"access": "restricted",
"baseBranch": "main",
"updateInternalDependencies": "patch",
"ignore": ["@repo/web", "@repo/api"]
}
Best Practices
- Workspace protocol: Use
workspace:*for internal deps - Shared configs: Centralize TypeScript, ESLint
- Build caching: Enable Turborepo remote caching
- Incremental builds: Use
dependsOncorrectly - Package exports: Use proper
exportsfield - Version management: Use Changesets
- Clean scripts: Include clean in each package
- Consistent naming: Use
@repo/prefix
Output Checklist
Every monorepo should include:
- pnpm-workspace.yaml configuration
- turbo.json with proper pipeline
- Shared TypeScript configs
- Shared ESLint configs
- UI component library
- Utility package
- Proper package.json exports
- tsup for library building
- CI/CD with caching
- Changeset for versioning
More from monkey1sai/openai-cli
multi-tenant-safety-checker
Ensures tenant isolation at query and policy level using Row Level Security, automated testing, and security audits. Prevents data leakage between tenants. Use for "multi-tenancy", "tenant isolation", "RLS", or "data security".
10modal-drawer-system
Implements accessible modals and drawers with focus trap, ESC to close, scroll lock, portal rendering, and ARIA attributes. Includes sample implementations for common use cases like edit forms, confirmations, and detail views. Use when building "modals", "dialogs", "drawers", "sidebars", or "overlays".
10eslint-prettier-config
Configures ESLint and Prettier for consistent code quality with TypeScript, React, and modern best practices. Use when users request "ESLint setup", "Prettier config", "linting configuration", "code formatting", or "lint rules".
9api-security-hardener
Hardens API security with rate limiting, input validation, authentication, and protection against common attacks. Use when users request "API security", "secure API", "rate limiting", "input validation", or "API protection".
9secure-headers-csp-builder
Implements security headers and Content Security Policy with safe rollout strategy (report-only → enforce), testing, and compatibility checks. Use for "security headers", "CSP", "HTTP headers", or "XSS protection".
9security-incident-playbook-generator
Creates response procedures for security incidents with containment steps, communication templates, and evidence collection. Use for "incident response", "security playbook", "breach response", or "IR plan".
9