Purpose
Guide the structure and orchestration of TypeScript monorepos with multiple packages including microservices, web apps, mobile apps, and shared libraries. Focus on high-level architecture, build orchestration, and development environment.
Scope boundaries:
- This skill: Monorepo structure, package organization, Docker Compose, build orchestration
- writing-clean-ts skill: Individual package code patterns (services, repositories, controllers)
- test-typescript skill: Testing patterns and coverage
When to Use
- Setting up a new monorepo with pnpm workspaces
- Structuring packages folder (microservices, web, mobile, libs)
- Configuring shared libraries (common, node-common, react-common)
- Setting up ES modules with path aliases
- Centralizing configuration (TypeScript, ESLint, Vitest)
- Orchestrating Docker Compose for development
- Managing inter-package dependencies
- Starting ANY new TypeScript project that might grow to multiple packages
When NOT to Use
- Single-package projects → Use
writing-clean-tsskill instead - Yarn or npm workspaces → This skill is pnpm-specific
- Non-TypeScript monorepos → Patterns here assume TypeScript throughout
- Existing monorepo with different conventions → Adapt patterns as needed
Quick Decision Guide
Choosing the right package type:
| You need... | Create... | Dependencies |
|---|---|---|
| Shared business models, utilities | packages/common/ |
None (pure) |
| Database, logging, messaging (Node.js) | packages/node-common/ |
common |
| Shared React components, hooks | packages/react-common/ |
common |
| Backend API/microservice | packages/node-{domain}/ |
common, node-common |
| Web application (Vite) | packages/react-{name}/ |
common, react-common |
| Mobile app (Expo) | packages/mobile-app/ |
common, react-common |
| Marketing/public site | packages/website/ |
common, react-common |
Dependency flow:
common → node-common → node-{service}
common → react-common → react-{app} / mobile-app
Monorepo Structure Overview
project-root/
├── packages/ # All packages live here
│ ├── common/ # Shared business models, utilities
│ ├── node-common/ # Node.js-specific utilities
│ ├── react-common/ # React shared components
│ ├── config/ # Shared ESLint, Prettier, TypeScript configs
│ ├── node-core/ # Backend microservice
│ ├── node-auth/ # Auth microservice
│ ├── react-admin/ # Admin web app
│ ├── react-core/ # Main web app
│ ├── mobile-app/ # Mobile app (React Native/Expo)
│ └── website/ # Marketing site (Next.js)
├── config/ # Root-level shared configs
│ ├── typescript/ # TypeScript base configs
│ ├── vitest/ # Vitest base configs
│ └── eslint/ # ESLint base configs
├── resource/
│ ├── script/ # Build and utility scripts
│ ├── docker/ # Dockerfiles
│ └── secret/ # Encrypted secrets
├── package.json # Root orchestration
├── pnpm-workspace.yaml # Workspace and catalog config
├── tsconfig.json # TypeScript project references
├── compose.yml # Docker Compose for development
└── .env.example # Environment template
Root Orchestration
package.json
{
"name": "project-name",
"type": "module",
"private": true,
"engines": {
"node": ">=22 <23",
"pnpm": ">=10 <11"
},
"scripts": {
"build": "tsc -b && pnpm -r build",
"clean": "pnpm -r clean",
"lint": "prettier . && eslint . && pnpm -r lint",
"test": "pnpm -r test",
"watch": "tsc -b -w",
"ws-build": "pnpm --filter",
"ws-test": "pnpm --filter"
},
"devDependencies": {
"@types/node": "catalog:",
"typescript": "catalog:",
"vitest": "catalog:",
"eslint": "catalog:",
"prettier": "catalog:"
},
"pnpm": {
"overrides": {
"@internal/msh-*": "link:../external-msh-monorepo/packages/$1"
}
}
}
Note: The
pnpm.overridessection shows how to link packages from an external monorepo. This is an example for a specific project setup - remove if not needed.
pnpm-workspace.yaml
packages:
- packages/*
catalog:
typescript: ^5.7
vitest: ^3.0
eslint: ^9.22
prettier: ^3.5
"@types/node": ^22
zod: ^3.24
express: ^5.0
react: ^19
"@mui/material": ^7
catalogProtocols:
# Example for external monorepo linking:
# msh-internal: link:
Note: Use
catalog:in package.json to reference these versions. This ensures consistency across all packages.
tsconfig.json (Project References)
{
"compilerOptions": {
"composite": true
},
"references": [
{ "path": "packages/common" },
{ "path": "packages/node-common" },
{ "path": "packages/react-common" },
{ "path": "packages/node-core" },
{ "path": "packages/react-admin" }
],
"files": []
}
Package Types Overview
| Package | Purpose | Dependencies |
|---|---|---|
common |
Shared business models, utilities | None (pure) |
node-common |
Node.js infrastructure (DB, logging, MQ) | common |
react-common |
Shared React components, hooks | common |
node-* |
Backend microservices | common, node-common |
react-* |
Frontend web apps | common, react-common |
mobile-app |
React Native/Expo app | common, react-common |
For detailed package configurations, see:
- Backend Packages - common, node-common, microservices
- Frontend Packages - react-common, web apps, mobile
ES Modules & Path Aliases
The #src/* Pattern
Each package uses path aliases for clean imports:
tsconfig.json:
{
"compilerOptions": {
"paths": {
"#src": ["./src"],
"#src/*": ["./src/*"]
}
}
}
package.json:
{
"imports": {
"#src/*": "./dist/*.js"
}
}
Usage in code:
import { someUtil } from '#src/util/some-util.js'
import { UserModel } from '#src/business/model/user/user-model.js'
Why this matters: The #src/* alias works both at compile-time (TypeScript paths) and runtime (Node.js subpath imports), so built code works without path rewriting issues.
Build Process
# 1. Clean dist
node ../../resource/script/npm/clean.js .
# 2. Compile TypeScript
tsc -p ./tsconfig.build.json
# 3. Resolve aliases (tsc-alias)
tsc-alias -p ./tsconfig.build.json
Inter-Package Dependencies
Dependency Hierarchy
@app/common (core models, utilities)
│
┌───────────────┼───────────────┐
│ │ │
▼ ▼ ▼
node-common node-core react-common
│ │ │
│ │ │
└───────────────┴───────────────┘
│
▼
Frontend Apps
(react-admin, react-core, mobile-app)
Workspace Dependencies
In package.json:
{
"dependencies": {
"@app/common": "workspace:*",
"@app/node-common": "workspace:*",
"@app/react-common": "workspace:*",
"@app/config": "workspace:*"
}
}
Catalog Dependencies
In package.json:
{
"dependencies": {
"zod": "catalog:",
"express": "catalog:",
"typeorm": "catalog:"
},
"devDependencies": {
"typescript": "catalog:",
"vitest": "catalog:"
}
}
Docker Compose Development
For full Docker setup, see Infrastructure Reference
Quick Start
# compose.yml - minimal example
services:
node-core:
build:
context: .
dockerfile: resource/docker/Dockerfile.node-base
command: pnpm run serve
working_dir: /app/packages/node-core
volumes:
- ./:/app:ro
depends_on:
pg: { condition: service_healthy }
labels:
- "traefik.http.routers.node-core.rule=PathPrefix(`/api/core`)"
Common Commands
docker compose up -d # Start all services
docker compose logs -f node-core # View logs
docker compose up -d --build node-core # Rebuild
docker compose down # Stop all
Process
1. Analyze Requirements
- Identify package types needed (services, apps, libs)
- Determine shared code boundaries
- Plan dependency hierarchy
2. Structure Packages
- Create packages in
./packages/folder - Set up common libs first (common, node-common, react-common)
- Configure package.json with proper exports and workspace deps
- Create tsconfig.json extending base configs
3. Configure Root
- Set up pnpm-workspace.yaml with catalog
- Configure root package.json scripts
- Create tsconfig.json with project references
- Set up centralized config in config/
4. Set Up Docker
- Create compose.yml with all services
- Configure Traefik routing
- Set up health checks
- Configure volume mounts for development
5. Verify Setup
pnpm install # Install dependencies
pnpm build # Build all packages
pnpm test # Run tests
docker compose up -d # Start development environment
Adding a New Package
Use this checklist when creating a new package:
Backend Package
- Create directory:
packages/node-{name}/ - Create
package.jsonwith:name:@app/node-{name}type:moduleexportsandimportsfor#src/*- Dependencies:
@app/common,@app/node-commonasworkspace:* - Build scripts
- Create
tsconfig.jsonextending../../config/typescript/tsconfig.base.json - Create
tsconfig.build.jsonfor production builds - Create
src/withapp/server-app.tsentry point - Add to root
tsconfig.jsonreferences - Add to
compose.ymlwith Traefik labels - Run
pnpm install
Frontend Package
- Create directory:
packages/react-{name}/ - Create
package.jsonwith:name:@app/react-{name}type:module- Dependencies:
@app/common,@app/react-commonasworkspace:* - Vite config for dev server
- Create
tsconfig.jsonextending../../config/typescript/tsconfig.react.base.json - Create
vite.config.tswithvite-tsconfig-pathsplugin - Create
src/withmain.tsxentry point - Add to root
tsconfig.jsonreferences - Add to
compose.ymlif needed - Run
pnpm install
Best Practices
Package Naming
- Use
@app/prefix for internal packages - Backend:
node-{domain}(e.g.,node-core,node-auth) - Frontend:
react-{purpose}(e.g.,react-admin,react-core) - Shared:
common,node-common,react-common
Version Management
- Use
catalog:for shared dependency versions - Use
workspace:*for internal packages - Keep versions consistent across packages
Build Order
- Build common libs first
- Build platform-specific libs second
- Build apps/services last
- Use TypeScript project references for correct order
Development Workflow
- Use Docker Compose for full stack development
- Use volume mounts for hot reload
- Use Traefik for routing between services
- Use health checks for service dependencies
Common Pitfalls
- Circular dependencies: Never let common depend on platform-specific libs
- Mixed module systems: All packages should use ES modules (
"type": "module") - Missing exports: Always define
exportsfield in package.json - Incorrect paths: Ensure tsconfig paths match package.json imports
- Catalog drift: Keep pnpm-workspace.yaml catalog versions consistent
- Missing .js extensions: ES modules require
.jsin import paths
Troubleshooting
ERR_PNPM_NO_MATCHING_VERSION
Cause: Package references a catalog: dependency that doesn't exist in pnpm-workspace.yaml
Fix: Add the missing package to the catalog, or use explicit version:
# pnpm-workspace.yaml
catalog:
your-missing-package: ^1.0.0
TypeScript project reference errors
Cause: A package is referenced in root tsconfig.json but doesn't exist or has errors
Fix:
- Ensure package exists in
packages/folder - Run
pnpm installto link workspace packages - Build dependencies first:
pnpm --filter @app/common build
Cannot find module '#src/...'
Cause: Path alias not configured correctly or package not built
Fix:
- Check
tsconfig.jsonhaspathsconfigured - Check
package.jsonhasimportsconfigured - Run
pnpm buildin the package - Ensure you're using
.jsextension in imports
ES module errors in built code
Cause: TypeScript emits imports without .js extensions
Fix: Run tsc-alias after build, or use the bundled fix script:
node scripts/build-esm-fix.js packages/your-package
Docker volume permission errors
Cause: Container runs as different user than host
Fix:
- Add
USER nodeto Dockerfile after install - Or run:
docker compose exec node-core chown -R node:node /app
Tests fail with "Cannot find module"
Cause: Vitest config doesn't resolve path aliases
Fix: Use vite-tsconfig-paths in vitest config:
import tsconfigPaths from 'vite-tsconfig-paths'
export default {
test: { ... },
plugins: [tsconfigPaths()]
}
Workspace package not found
Cause: workspace:* dependency not installed
Fix:
- Run
pnpm installat root - Check package name matches exactly in
package.json - Verify package is in
packages/folder
Bundled Scripts
This skill includes helper scripts in scripts/:
clean.js- Removesdist/folderbuild-esm-fix.js- Adds.jsextensions to ES module imports
Copy these to resource/script/npm/ in your project and reference in package.json:
{
"scripts": {
"build": "node ../../resource/script/npm/clean.js . && tsc -p ./tsconfig.build.json && tsc-alias -p ./tsconfig.build.json"
}
}
References
- Backend Packages - Detailed config for common, node-common, microservices
- Frontend Packages - Detailed config for react-common, web apps, mobile
- Infrastructure - Docker Compose, build scripts, centralized configs