The Developer's Guide to Building Production-Ready Monorepos

Table of contents
- What Makes Monorepos Special?
- Setting Up Your Development Environment
- Step 1: Creating Your Monorepo Foundation
- Step 2: Installing Essential Dependencies
- Step 3: Configuring Code Quality Tools
- Step 4: Configuring Package Management
- Step 5: Setting Up Turborepo
- Step 6: Creating Your First Application
- Step 7: Using Shared Packages
- Step 8: Development Workflow
- Step 9: Building and Testing
- Step 10: Advanced Features
- Performance Optimizations
- Common Patterns and Best Practices
- Troubleshooting Common Issues
- Taking Your Monorepo to Production
- Real-World Examples
- Beyond the Basics
- The Road Ahead
- References and Further Reading

Picture this: You're managing five different JavaScript projects, each with its own repository, package.json, and deployment pipeline. Sound familiar? Every time you need to update a shared utility function, you're copy-pasting code across repositories, dealing with version mismatches, and watching your CI/CD costs skyrocket. There's a better way.
Welcome to the world of monorepos, where Google manages over 2 billion lines of code in a single repository, and companies like Facebook, Microsoft, and Netflix have revolutionized their development workflows. Today, we'll build a production-ready monorepo from scratch that will transform how you think about project organization.
What Makes Monorepos Special?
Before we dive into the technical details, let's understand why monorepos have become the secret weapon of successful development teams. Unlike traditional multi-repository setups where each project lives in isolation, a monorepo houses multiple related projects under one roof while maintaining clear boundaries between them.
Think of it as moving from individual apartments to a well-designed co-working space. Everyone has their own office (project), but they share common facilities (utilities, configurations, and tools). This approach eliminates the coordination nightmare of managing dependencies across separate repositories while preserving the modularity that keeps code organized.
Setting Up Your Development Environment
Let's start by preparing your machine for monorepo development. We'll use modern tools that make the process smooth and enjoyable.
Prerequisites Check
First, ensure your system meets these requirements:
# Check Node.js version (needs v18.0.0 or higher)
node --version
# Check Git version (needs v2.30.0 or higher)
git --version
# Install Bun (our preferred package manager)
curl -fsSL https://bun.sh/install | bash
If you prefer Yarn or npm, that works too, but Bun offers significantly faster installation and execution times.
Step 1: Creating Your Monorepo Foundation
Let's start by using the battle-tested template that powers many production applications:
# Clone the proven template
git clone https://github.com/John-pels/monorepo-template.git awesome-monorepo
cd awesome-monorepo
# Clean slate - remove existing git history
rm -rf .git
git init
git add .
git commit -m "feat: initialize monorepo from template"
Alternatively, you can use the template directly on GitHub by clicking "Use this template" – perfect if you want to start with a clean repository right away.
Understanding the Architecture
Your new monorepo follows a carefully designed structure:
awesome-monorepo/
├── apps/ # Your applications live here
│ ├── web/ # Next.js frontend (port 8000)
│ ├── dashboard/ # Vite + TanStack Router admin (port 8001)
│ └── mobile/ # React Native app
├── packages/ # Shared libraries and utilities
│ ├── ui/ # Reusable React components
│ ├── utils/ # Business logic and helpers
│ ├── eslint-config/ # Consistent linting rules
│ ├── jest-config/ # Testing configurations
│ └── types/ # TypeScript definitions
├── docs/ # Project documentation
└── tools/ # Build and development utilities
This structure follows the principle of separation of concerns – applications are consumer-facing products, while packages contain reusable code that applications can depend on.
Step 2: Installing Essential Dependencies
Now let's equip your monorepo with the tools that ensure code quality and smooth development:
# Install code quality tools
bun add -D @biomejs/biome
# Add git hooks for automated checks
bun add -D husky lint-staged
# Set up commit message validation
bun add -D @commitlint/cli @commitlint/config-conventional
Why These Tools Matter
Biome: A lightning-fast alternative to ESLint and Prettier that handles both linting and formatting
Husky: Prevents bad commits from entering your repository
lint-staged: Only runs checks on files you've changed
Commitlint: Enforces consistent commit messages that work with automated changelogs
Step 3: Configuring Code Quality Tools
Setting Up Biome
Create a biome.json
file in your root directory with this configuration:
{
"$schema": "https://biomejs.dev/schemas/1.8.3/schema.json",
"vcs": {
"enabled": true,
"clientKind": "git",
"useIgnoreFile": true
},
"organizeImports": {
"enabled": true
},
"linter": {
"enabled": true,
"ignore": ["./apps/dashboard/src/routeTree.gen.ts"],
"rules": {
"recommended": true,
"correctness": {
"noUnusedVariables": "error",
"noUnusedImports": "error"
},
"security": {
"noDangerouslySetInnerHtml": "off"
},
"suspicious": {
"noConsoleLog": "warn"
}
}
},
"formatter": {
"enabled": true,
"lineWidth": 80,
"indentStyle": "space",
"indentWidth": 2
},
"javascript": {
"formatter": {
"semicolons": "asNeeded",
"trailingCommas": "es5",
"quoteStyle": "single"
}
}
}
This configuration provides sensible defaults while being strict about code quality. The ignore
array excludes auto-generated files from checks.
Configuring Git Hooks
Initialize Husky and set up your git hooks:
# Initialize Husky
npx husky init
# Create pre-commit hook
echo "npx lint-staged" > .husky/pre-commit
# Create commit message validation hook
echo "npx commitlint --edit \$1" > .husky/commit-msg
# Make hooks executable
chmod +x .husky/pre-commit
chmod +x .husky/commit-msg
Setting Up Commit Message Standards
Create a commitlint.config.js
file:
module.exports = {
extends: ['@commitlint/config-conventional'],
rules: {
'body-leading-blank': [1, 'always'],
'body-max-line-length': [2, 'always', 100],
'footer-leading-blank': [1, 'always'],
'footer-max-line-length': [2, 'always', 100],
'header-max-length': [2, 'always', 100],
'scope-case': [2, 'always', 'lower-case'],
'subject-case': [
2,
'never',
['sentence-case', 'start-case', 'pascal-case', 'upper-case'],
],
'subject-empty': [2, 'never'],
'subject-full-stop': [2, 'never', '.'],
'type-case': [2, 'always', 'lower-case'],
'type-empty': [2, 'never'],
'type-enum': [
2,
'always',
[
'build',
'chore',
'ci',
'docs',
'feat',
'fix',
'perf',
'refactor',
'revert',
'style',
'test',
'translation',
'security',
'changeset',
],
],
},
}
Step 4: Configuring Package Management
Update your root package.json
with essential scripts and workspace configuration:
{
"name": "awesome-monorepo",
"private": true,
"packageManager": "bun@1.2.19",
"workspaces": ["apps/*", "packages/*"],
"scripts": {
"build": "turbo run build",
"dev": "dotenv -- turbo run dev",
"lint": "biome check --write --unsafe",
"format": "biome format --write .",
"format-and-lint:fix": "biome check --write --unsafe",
"test": "turbo run test",
"check-types": "turbo run check-types",
"prepare": "husky"
},
"lint-staged": {
"*.{js,jsx,ts,tsx,json}": [
"biome check --apply --no-errors-on-unmatched"
]
}
}
The workspaces
field tells your package manager where to find sub-projects, while the scripts provide convenient commands for common operations.
Step 5: Setting Up Turborepo
Turborepo is the engine that makes your monorepo fast and efficient. Create a turbo.json
configuration:
{
"$schema": "https://turborepo.com/schema.json",
"ui": "tui",
"tasks": {
"build": {
"dependsOn": ["^build"],
"inputs": ["$TURBO_DEFAULT$", ".env*"],
"outputs": [".next/**", "!.next/cache/**"]
},
"lint": {
"dependsOn": ["^lint"]
},
"check-types": {
"dependsOn": ["^check-types"]
},
"dev": {
"cache": false,
"persistent": true
}
}
}
This configuration defines task relationships and caching strategies. The ^build
syntax means "run build on dependencies first."
Step 6: Creating Your First Application
Let's add a Next.js application to demonstrate the workflow:
cd apps
npx create-next-app@latest web --typescript --tailwind --eslint --app --src-dir
cd web
Connecting to Shared Packages
Edit the apps/web/package.json
to include references to your shared packages:
{
"dependencies": {
"@repo/ui": "*",
"@repo/utils": "*",
"next": "latest",
"react": "latest",
"react-dom": "latest"
},
"devDependencies": {
"@repo/eslint-config": "*",
"@repo/typescript-config": "*"
}
}
The "*"
version tells the package manager to use the local workspace version.
Creating a Shared UI Package
Build a reusable component library:
mkdir -p packages/ui/src
cd packages/ui
# Initialize package
bun init -y
Create packages/ui/package.json
:
{
"name": "@repo/ui",
"version": "0.0.0",
"main": "./src/index.ts",
"types": "./src/index.ts",
"exports": {
".": "./src/index.ts"
},
"peerDependencies": {
"react": "^18.0.0"
},
"devDependencies": {
"@repo/eslint-config": "*",
"@repo/typescript-config": "*"
}
}
Add a simple button component in packages/ui/src/button.tsx
:
import { ReactNode } from 'react'
interface ButtonProps {
children: ReactNode
onClick?: () => void
variant?: 'primary' | 'secondary'
}
export function Button({
children,
onClick,
variant = 'primary'
}: ButtonProps) {
const baseClasses = 'px-4 py-2 rounded font-medium transition-colors'
const variantClasses = {
primary: 'bg-blue-600 text-white hover:bg-blue-700',
secondary: 'bg-gray-200 text-gray-900 hover:bg-gray-300'
}
return (
<button
className={`${baseClasses} ${variantClasses[variant]}`}
onClick={onClick}
>
{children}
</button>
)
}
Export it in packages/ui/src/index.ts
:
export { Button } from './button'
Step 7: Using Shared Packages
Now use your shared button in the Next.js app. Edit apps/web/src/app/page.tsx
:
import { Button } from '@repo/ui'
export default function Home() {
return (
<main className="flex min-h-screen flex-col items-center justify-center p-24">
<h1 className="text-4xl font-bold mb-8">Welcome to Our Monorepo!</h1>
<Button
onClick={() => alert('Hello from shared UI!')}
variant="primary"
>
Click me!
</Button>
</main>
)
}
Step 8: Development Workflow
Install all dependencies and start development:
# Install all dependencies across workspaces
bun install
# Start all applications in development mode
bun dev
# Or start a specific app
bun dev --filter=web
The beauty of this setup is hot reloading across packages. Change your button component, and see it update instantly in your Next.js app!
Step 9: Building and Testing
Running Builds
# Build everything
bun run build
# Build specific app and its dependencies
bun run build --filter=web
# Check for TypeScript errors
bun run check-types
Setting Up Tests
Create a shared Jest configuration in packages/jest-config/index.js
:
module.exports = {
testEnvironment: 'jsdom',
setupFilesAfterEnv: ['<rootDir>/jest.setup.js'],
moduleNameMapping: {
'^@repo/(.*)$': '<rootDir>/../../packages/$1/src'
},
collectCoverageFrom: [
'src/**/*.{ts,tsx}',
'!src/**/*.d.ts'
]
}
Add tests for your button component in packages/ui/src/__tests__/button.test.tsx
:
import { render, screen, fireEvent } from '@testing-library/react'
import { Button } from '../button'
describe('Button', () => {
it('renders with correct text', () => {
render(<Button>Click me</Button>)
expect(screen.getByText('Click me')).toBeInTheDocument()
})
it('calls onClick when clicked', () => {
const handleClick = jest.fn()
render(<Button onClick={handleClick}>Click me</Button>)
fireEvent.click(screen.getByText('Click me'))
expect(handleClick).toHaveBeenCalledTimes(1)
})
})
Step 10: Advanced Features
Dependency Management
Add dependencies strategically:
# Add to specific workspace
bun workspace @repo/ui add lucide-react
# Add to root (affects all workspaces)
bun add -D typescript
# Add to multiple workspaces
bun workspaces foreach add axios
Environment Variables
Create environment-specific configurations:
# Root .env
NODE_ENV=development
DATABASE_URL=postgresql://localhost:5432/dev
# App-specific .env.local
NEXT_PUBLIC_API_URL=http://localhost:3001
CI/CD Integration
Create .github/workflows/ci.yml
:
name: CI
on: [push, pull_request]
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: oven-sh/setup-bun@v1
with:
bun-version: latest
- name: Install dependencies
run: bun install --frozen-lockfile
- name: Lint code
run: bun run lint
- name: Type check
run: bun run check-types
- name: Run tests
run: bun run test
- name: Build applications
run: bun run build
Performance Optimizations
Caching Strategy
Turborepo's intelligent caching saves significant time:
# First build (slow)
bun run build
# Second build (lightning fast - uses cache)
bun run build
# Force rebuild (ignores cache)
bun run build --force
Selective Operations
Run commands only on changed packages:
# Only lint changed files
bun run lint --filter=[HEAD^1]
# Build only affected packages
bun run build --filter=...[origin/main]
Common Patterns and Best Practices
Package Naming
Use consistent naming conventions:
@your-company/ui # UI components
@your-company/utils # Utilities
@your-company/config # Configurations
@your-company/types # TypeScript types
Dependency Management
Keep shared dependencies in the root
package.json
Use exact versions for critical dependencies
Regular dependency audits with
bun audit
Code Organization
packages/ui/
├── src/
│ ├── components/ # Reusable components
│ ├── hooks/ # Custom React hooks
│ ├── utils/ # Utility functions
│ └── index.ts # Main export file
├── package.json
└── tsconfig.json
Troubleshooting Common Issues
Installation Problems
# Clear all caches and reinstall
bun pm cache rm
rm -rf node_modules
rm bun.lockb
bun install
Build Failures
# Check TypeScript errors
bun run check-types
# Clear Turborepo cache
bun run build --force
# Verbose output for debugging
bun run build --verbose
Development Server Issues
# Check port conflicts
lsof -i :3000
# Clear Next.js cache
rm -rf apps/web/.next
# Restart with clean cache
bun dev --filter=web
Taking Your Monorepo to Production
Deployment Strategies
Each application can deploy independently:
# Deploy web app to Vercel
cd apps/web
vercel deploy
# Deploy API to Railway
cd apps/api
railway deploy
Monitoring and Analytics
Consider adding tools like:
Sentry for error tracking
Datadog for performance monitoring
Bundle Analyzer for bundle optimization
Real-World Examples
This template powers several production applications. You can:
Explore the complete codebase on GitHub
Use the template for your next project
Read the documentation for advanced configurations
Beyond the Basics
Micro-frontends
Your monorepo can easily evolve into a micro-frontend architecture:
# Add module federation support
bun add @module-federation/nextjs-mf
# Create shell application
mkdir apps/shell
Mobile Integration
Add React Native applications seamlessly:
cd apps
npx create-expo-app mobile --template blank-typescript
Backend Services
Include Node.js APIs and databases:
mkdir apps/api
cd apps/api
bun init -y
bun add express cors helmet
The Road Ahead
Monorepos represent the future of scalable application development. Companies using this approach report Faster development cycle, Reduction in dependency management overhead, Fewer integration issues, improved code reuse, and consistency. Your monorepo journey doesn't end here. As your projects grow, you'll discover new patterns, tools, and optimizations. The foundation you've built today will adapt and scale with your needs.
The monorepo template provides a battle-tested starting point, but the real magic happens when you adapt it to your specific needs. Whether you're building the next unicorn startup or managing enterprise applications, monorepos offer the flexibility and power to scale your development efforts effectively.
References and Further Reading
Turborepo Documentation - Official guide to build orchestration
Bun Workspaces - Package management patterns
Vite.js - For creating Vite-powered applications
Biome Documentation - Modern linting and formatting
Next.js Monorepo Guide - Framework-specific patterns
Google's Monorepo Philosophy - Academic insights from industry leaders
Husky Documentation - Git hooks automation
Commitlint - Commit message conventions
Start your monorepo journey today with the proven template and join thousands of developers who've streamlined their development workflows. Your future self will thank you for making the switch!
Ready to transform your development workflow? Get started with the monorepo template and experience the power of unified development today.
Subscribe to my newsletter
Read articles from John O. Emmanuel directly inside your inbox. Subscribe to the newsletter, and don't miss out.
Written by

John O. Emmanuel
John O. Emmanuel
A Versatile Senior Software Engineer with over 5 years of expertise in full-stack development, focusing on React, React-Native, Node.js, Golang, and TypeScript ecosystems. Demonstrated success in spearheading remote teams and architecting high-performance, scalable web and mobile applications. Exceptional communicator skilled in fostering collaboration across diverse time zones and cultural backgrounds. Committed to driving innovation and operational excellence in distributed work environments.