Monorepo Management
Overview
A monorepo is a trade:
- •✅ Shared tooling and unified refactors
- •✅ Code reuse across apps/packages
- •✅ Single source of truth for types and utilities
- •❌ Larger dependency graphs (harder to reason about)
- •❌ Build and CI complexity if not designed intentionally
Buzz Stack is a single Next.js app repo, but many teams evolve it into a monorepo when they add:
- •a shared design system
- •shared API contracts / types
- •background workers or separate deployables
- •multiple Next.js apps
This skill focuses on a pragmatic “pnpm-first” approach.
Core Concepts
1) Workspace Organization: Packages, Apps, and Boundaries
A common structure:
. ├─ apps/ │ ├─ web/ # Next.js app (Buzz Stack) │ └─ admin/ # optional second app ├─ packages/ │ ├─ ui/ # design system components │ ├─ types/ # shared TypeScript types │ ├─ config/ # shared eslint/tsconfig/tailwind presets │ └─ utils/ # pure utilities └─ pnpm-workspace.yaml
Principles:
- •Keep “deployables” (apps/services) in
apps/ - •Keep “libraries” in
packages/ - •Enforce boundaries so apps depend on packages, not the other way around
2) pnpm Workspaces: The Contract
pnpm-workspace.yaml declares package locations.
packages: - "apps/*" - "packages/*"
Why pnpm matters:
- •pnpm’s store + symlink model discourages undeclared dependencies
- •workspace protocol (
workspace:*) makes internal deps explicit
3) Dependency Management Strategy
Your goal is to keep the dependency graph:
- •shallow
- •acyclic
- •explicit
Key tools:
- •
workspace:*protocol - •peer dependencies for host-controlled libs (React, Next)
- •constraints/lint rules to enforce boundaries
Example: internal dependency via workspace protocol.
{
"name": "@acme/web",
"dependencies": {
"@acme/ui": "workspace:*",
"@acme/types": "workspace:*"
}
}
Peer deps example for UI package:
{
"name": "@acme/ui",
"peerDependencies": {
"react": "^19",
"react-dom": "^19"
}
}
4) Shared Tooling: ESLint, TypeScript, Tailwind
Monorepos should centralize configuration, then consume it.
TypeScript pattern:
- •root
tsconfig.base.json - •each package has
tsconfig.jsonthat extends base
// tsconfig.base.json
{
"compilerOptions": {
"strict": true,
"target": "ES2022",
"module": "ESNext",
"moduleResolution": "Bundler",
"noEmit": true
}
}
// packages/ui/tsconfig.json
{
"extends": "../../tsconfig.base.json",
"compilerOptions": {
"jsx": "react-jsx"
},
"include": ["src/**/*.ts", "src/**/*.tsx"]
}
ESLint flat config:
- •export a shared flat config from
packages/config/eslint/ - •each package imports and extends it
Buzz Stack already uses ESLint 9 flat config at the repo root (see eslint.config.mjs).
5) Build Strategies: Incremental, Cached, and “Affected”
Monorepo builds break down if everything rebuilds on every change.
Common approaches:
- •TurboRepo pipelines (
turbo.json) - •Nx affected builds
- •custom scripts using git diff
Turbo example:
// turbo.json
{
"$schema": "https://turbo.build/schema.json",
"pipeline": {
"lint": {
"outputs": []
},
"build": {
"dependsOn": ["^build"],
"outputs": ["dist/**", ".next/**"]
},
"test": {
"dependsOn": ["^build"],
"outputs": []
}
}
}
6) Release and Versioning Coordination
You have two primary strategies:
- •
Single version (locked-step):
- •simpler for internal-only repos
- •one release tag
- •
Independent versions:
- •necessary if you publish packages separately
- •requires tooling (changesets, semantic-release)
Pattern: use conventional commits consistently.
feat(ui): add accessible dropdown fix(web): avoid double fetch in server component chore(config): align eslint rules
Patterns (10+)
Pattern 1: Minimal pnpm Workspace Setup
# pnpm-workspace.yaml packages: - "apps/*" - "packages/*"
// package.json (root)
{
"private": true,
"packageManager": "pnpm@9.0.0",
"scripts": {
"lint": "pnpm -r lint",
"build": "pnpm -r build",
"test": "pnpm -r test"
}
}
Pattern 2: Package Boundary Rules (No Deep Imports)
Enforce that consumers import from package entrypoints.
// ✅ good
import { Button } from "@acme/ui";
// ❌ bad
import { Button } from "@acme/ui/src/components/Button";
Why:
- •deep imports bypass semver
- •break builds when file layout changes
Pattern 3: Shared @acme/types Package
Keep shared DTOs and domain types in a dedicated package.
// packages/types/src/user.ts
export interface UserDto {
id: string;
email: string;
name: string;
}
Apps and services depend on the same types.
Pattern 4: Shared ESlint Flat Config Package
// packages/config/eslint/index.mjs import js from "@eslint/js"; export const baseConfig = [js.configs.recommended];
Each package:
// apps/web/eslint.config.mjs
import { baseConfig } from "@acme/config-eslint";
export default [...baseConfig];
Pattern 5: Shared Tailwind Preset Package
// packages/config/tailwind/preset.ts
import type { Config } from "tailwindcss";
const preset: Partial<Config> = {
theme: {
extend: {},
},
};
export default preset;
Consume:
// apps/web/tailwind.config.ts
import type { Config } from "tailwindcss";
import preset from "@acme/config-tailwind";
const config: Config = {
presets: [preset],
content: ["./app/**/*.{ts,tsx}", "./components/**/*.{ts,tsx}"],
};
export default config;
Pattern 6: Workspace Protocol Everywhere for Internal Packages
{
"dependencies": {
"@acme/utils": "workspace:*"
}
}
Why:
- •prevents accidental publishing constraints
- •ensures local dev uses the local package
Pattern 7: Peer Dependencies for React/Next in Shared UI Packages
{
"peerDependencies": {
"react": "^19.0.0",
"react-dom": "^19.0.0"
},
"devDependencies": {
"react": "^19.0.0",
"react-dom": "^19.0.0"
}
}
Pattern 8: Build Outputs and exports for Libraries
{
"name": "@acme/utils",
"type": "module",
"main": "./dist/index.js",
"types": "./dist/index.d.ts",
"exports": {
".": {
"types": "./dist/index.d.ts",
"default": "./dist/index.js"
}
}
}
Pattern 9: Affected Builds in CI (Turbo/Nx)
Pseudo pipeline:
- •compute changed packages
- •build only impacted apps
changed: packages/types affected: packages/ui, apps/web
Pattern 10: Keep Server-Only and Client-Only Code Separate
In Next.js monorepos, split packages by runtime:
- •
@acme/server-*packages can use Node APIs - •
@acme/client-*packages must be browser-safe
Pattern 11: Consistent TypeScript Settings Across Packages
{
"compilerOptions": {
"strict": true,
"noUncheckedIndexedAccess": true
}
}
Avoid “weak” packages that leak unsoundness.
Pattern 12: Tooling Scripts as a Package
Buzz Stack already uses orchestration scripts (see scripts/subagent-toolkit.mjs).
In monorepos, centralize similar tooling in packages/tooling/.
Anti-Patterns
Anti-pattern 1: Cyclic Dependencies Between Packages
If ui -> types -> ui, you will suffer.
Fix:
- •extract shared primitives into a third package
- •enforce dependency direction rules
Anti-pattern 2: Hidden Dependencies via Hoisting Assumptions
If code works only because Node resolves a transitive dependency, it will break.
Fix:
- •declare deps explicitly
- •rely on pnpm strictness to catch missing deps
Anti-pattern 3: Publishing Internal Packages Without Build/Exports Discipline
Fix:
- •produce stable
dist/ - •define
exports - •test consumer imports
Anti-pattern 4: Multiple TS/ESLint Config Dialects Across Packages
Fix:
- •centralize config packages
- •extend them everywhere
Anti-pattern 5: Shared Package That Imports App Code
Fix:
- •keep shared packages app-agnostic
- •pass dependencies in via interfaces
Real-World Buzz Stack Anchors
Even in a single-repo setup, Buzz Stack demonstrates monorepo-adjacent patterns:
- •pnpm is the declared package manager (
package.json) - •ESLint 9 flat config is centralized (
eslint.config.mjs) - •orchestration tooling lives in
scripts/(useful precursor topackages/tooling)
When converting Buzz Stack to a monorepo, keep these ideas:
- •one linting dialect
- •strict TS everywhere
- •explicit package boundaries
Cross-References
- •
Skills:
- •
buzz-stack-dev-workflow: ../buzz-stack-dev-workflow/SKILL.md - •
eslint-flat-config-workflow: ../eslint-flat-config-workflow/SKILL.md - •
architecture-design: ../architecture-design/SKILL.md - •
performance-profiling: ../performance-profiling/SKILL.md
- •
- •
Docs:
- •Code structure: ../../../docs/CODE_STRUCTURE.md
- •Development workflow: ../../../docs/DEVELOPMENT.md
Quick Checklist
- •Is
pnpm-workspace.yamlexplicit and minimal? - •Are internal deps
workspace:*? - •Do packages have clear runtime constraints?
- •Is build cached and incremental?
- •Are releases predictable (single vs independent versioning decided)?