Monorepos have become the architecture of choice for TypeScript projects at scale. As Rishikesh Baidya, our CTO, notes: "A well-structured monorepo lets teams move faster—shared code, atomic commits, and unified tooling pay dividends as you scale." At Softechinfra, we've built multiple production monorepos for complex SaaS applications.
Why Monorepos for TypeScript?
Tool Comparison: Turborepo vs Nx
| Feature | Turborepo | Nx |
|---|---|---|
| Setup Complexity | Simple, minimal config | More configuration options |
| Build Caching | Excellent, hash-based | Excellent, computation caching |
| Affected Commands | --filter flag | Built-in affected:* |
| Remote Caching | Vercel (free tier) | Nx Cloud (free tier) |
| Best For | Getting started, medium teams | Large orgs, enterprise needs |
Project Setup
Directory Structure
my-monorepo/
├── apps/
│ ├── web/ # Next.js frontend
│ ├── api/ # Express/Fastify backend
│ └── admin/ # Admin dashboard
├── packages/
│ ├── ui/ # Shared React components
│ ├── shared/ # Shared types and utilities
│ ├── database/ # Prisma schema and client
│ └── config/ # Shared configs (tsconfig, eslint)
├── package.json
├── pnpm-workspace.yaml
├── turbo.json
└── tsconfig.jsonWorkspace Configuration
packages:
- 'apps/*'
- 'packages/*'{
"$schema": "https://turbo.build/schema.json",
"pipeline": {
"build": {
"dependsOn": ["^build"],
"outputs": ["dist/", ".next/"]
},
"dev": {
"cache": false,
"persistent": true
},
"test": {
"dependsOn": ["build"],
"outputs": ["coverage/"]
},
"lint": {
"outputs": []
}
}
}TypeScript Configuration
Shared Base Config
packages/config/tsconfig.base.json:
{
"compilerOptions": {
"strict": true,
"esModuleInterop": true,
"skipLibCheck": true,
"forceConsistentCasingInFileNames": true,
"declaration": true,
"declarationMap": true,
"sourceMap": true,
"moduleResolution": "bundler",
"target": "ES2022",
"lib": ["ES2022"]
}
}Package Config Extension
apps/web/tsconfig.json:
{
"extends": "@myorg/config/tsconfig.base.json",
"compilerOptions": {
"outDir": "dist",
"rootDir": "src",
"jsx": "react-jsx"
},
"include": ["src//*"],
"exclude": ["node_modules", "dist"]
}Internal Package Patterns
Shared Package Setup
packages/shared/package.json:
{
"name": "@myorg/shared",
"version": "0.0.0",
"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"
}
}Shared Types Example
packages/shared/src/types.ts:
// Domain types shared across apps
export interface User {
id: string
email: string
name: string
role: 'admin' | 'user' | 'viewer'
}export interface ApiResponse {
data: T
meta: {
requestId: string
timestamp: number
}
}
// Utility types
export type Nullable = T | null
export type AsyncFunction = () => Promise
Using Internal Packages
apps/web/package.json:
{
"dependencies": {
"@myorg/shared": "workspace:*",
"@myorg/ui": "workspace:*"
}
}Build Optimization
Caching Strategies
Affected Builds Command
# Build only packages affected since last commit
turbo run build --filter=[HEAD^1]# Build only packages affected compared to main
turbo run build --filter=[origin/main...]
# Build specific package and its dependencies
turbo run build --filter=@myorg/web...
CI/CD Configuration
GitHub Actions with Remote Caching
name: CI
on: [push, pull_request]jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 2 # Needed for affected commands
- uses: pnpm/action-setup@v3
with:
version: 8
- uses: actions/setup-node@v4
with:
node-version: 20
cache: 'pnpm'
- run: pnpm install --frozen-lockfile
# Remote caching with Vercel
- run: pnpm turbo build test lint --filter=[HEAD^1]
env:
TURBO_TOKEN: ${{ secrets.TURBO_TOKEN }}
TURBO_TEAM: ${{ vars.TURBO_TEAM }}
Common Patterns
Checklist for Monorepo Success
- Keep packages small and focused—single responsibility
- Use workspace:* for internal dependencies
- Share ESLint/Prettier configs via internal package
- Set up remote caching from day one
- Use TypeScript project references for faster type checking
- Document package purposes in README files
When to Split Packages
- Good candidates for separate packages:
- UI components — shared across multiple apps
- API clients — generated or hand-written
- Utility functions — date helpers, validators
- Types/interfaces — domain models
- Config files — tsconfig, eslint, prettier
Projects like ChipMakerHub demonstrate this pattern with shared UI components across customer and admin portals.
Need Help Setting Up Your Monorepo?
We help teams architect and implement TypeScript monorepos that scale—from initial setup to CI/CD optimization.
Discuss Your Architecture →