Monorepos with Nx and Turborepo: How Large Companies Manage Projects in 2025
Hello HaWkers, if you've ever worked at a company with multiple related projects - a mobile app, an admin dashboard, a public website, an API - you've probably felt the pain of maintaining duplicated code, misaligned versions of shared libraries, and deployments that seem like an orchestra without a conductor.
Have you ever wondered how companies like Google, Facebook, and Microsoft manage hundreds of projects that share code without falling into chaos? And more importantly: how can you apply these same strategies to your projects, even in smaller teams?
What Are Monorepos and Why They Matter in 2025
A monorepo is a code organization strategy where multiple related projects live in the same Git repository. Instead of having one repository for each app, you have a single repository containing all your projects, sharing code, tools, and pipelines.
In 2025, monorepos have gone from being a curiosity of tech giants to becoming mainstream. The tools have matured, documentation has improved, and the benefits have become impossible to ignore:
Clear advantages:
- Simplified code sharing: Share libraries between projects without publishing to npm
- Atomic refactoring: Change an API and update all consumers in the same commit
- Consistency: Same dependency versions, same tools, same standards
- Visibility: See how changes affect all projects
- Optimized CI/CD: Run tests and builds only on what changed
Challenges solved in 2025:
- Tool performance (Nx and Turborepo are 7x+ faster)
- Setup complexity (ready templates and clear documentation)
- Scalability (support for thousands of projects)
Nx: The Enterprise-Grade Solution
Nx is a powerful monorepo tool, created by Nrwl, focused on scalability and developer experience. In 2025, Nx has consolidated as the choice for complex and polyglot (multiple languages) monorepos.
Nx Features
- Intelligent task orchestration: Nx understands dependencies between projects and executes tasks in the correct order
- Computation caching: Build and test results are cached locally and remotely
- Affected commands: Execute tasks only on projects affected by changes
- Plugins for everything: React, Angular, Node, Go, Rust, and more
- Dependency graph: Interactive visualization of how projects relate
Setting Up an Nx Monorepo
# Create new Nx workspace
npx create-nx-workspace@latest my-workspace
# Choose options:
# - Package-based monorepo or Integrated monorepo
# - TypeScript/JavaScript
# - CI/CD provider (GitHub Actions, GitLab, etc)
cd my-workspace
# Add applications
nx g @nx/react:app web
nx g @nx/react:app admin
nx g @nx/node:app api
# Add shared libraries
nx g @nx/js:lib shared-ui
nx g @nx/js:lib shared-utils
nx g @nx/js:lib data-accessNx Monorepo Structure
my-workspace/
├── apps/
│ ├── web/ # React app for public site
│ ├── admin/ # Admin dashboard
│ └── api/ # Node.js backend
├── libs/
│ ├── shared-ui/ # Shared UI components
│ ├── shared-utils/ # Shared utilities
│ └── data-access/ # Shared API client
├── tools/
│ └── generators/ # Custom generators
├── nx.json # Nx configuration
├── package.json
└── tsconfig.base.jsonnx.json Configuration
{
"extends": "nx/presets/npm.json",
"tasksRunnerOptions": {
"default": {
"runner": "nx/tasks-runners/default",
"options": {
"cacheableOperations": ["build", "test", "lint"],
"parallel": 3
}
}
},
"targetDefaults": {
"build": {
"dependsOn": ["^build"],
"outputs": ["{projectRoot}/dist"]
},
"test": {
"cache": true
}
},
"namedInputs": {
"default": ["{projectRoot}/**/*"],
"production": ["!{projectRoot}/**/*.spec.ts"]
}
}
Working with Shared Libraries
// libs/shared-ui/src/lib/Button.tsx
import React from 'react';
export interface ButtonProps {
variant?: 'primary' | 'secondary';
onClick?: () => void;
children: React.ReactNode;
}
export function Button({ variant = 'primary', onClick, children }: ButtonProps) {
return (
<button
className={`btn btn-${variant}`}
onClick={onClick}
>
{children}
</button>
);
}
// libs/shared-ui/src/index.ts - Barrel export
export * from './lib/Button';
export * from './lib/Input';
export * from './lib/Modal';Using in apps:
// apps/web/src/app/app.tsx
import { Button } from '@my-workspace/shared-ui';
export function App() {
return (
<div>
<h1>Welcome to Web App</h1>
<Button variant="primary" onClick={() => console.log('Clicked')}>
Click Me
</Button>
</div>
);
}Shared Data Access Layer
// libs/data-access/src/lib/api-client.ts
export class ApiClient {
private baseUrl: string;
constructor(baseUrl: string) {
this.baseUrl = baseUrl;
}
async get<T>(endpoint: string): Promise<T> {
const response = await fetch(`${this.baseUrl}${endpoint}`);
if (!response.ok) {
throw new Error(`API Error: ${response.statusText}`);
}
return response.json();
}
async post<T, D>(endpoint: string, data: D): Promise<T> {
const response = await fetch(`${this.baseUrl}${endpoint}`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify(data),
});
if (!response.ok) {
throw new Error(`API Error: ${response.statusText}`);
}
return response.json();
}
}
// libs/data-access/src/lib/users-service.ts
import { ApiClient } from './api-client';
export interface User {
id: string;
name: string;
email: string;
}
export class UsersService {
constructor(private api: ApiClient) {}
async getUsers(): Promise<User[]> {
return this.api.get<User[]>('/users');
}
async getUser(id: string): Promise<User> {
return this.api.get<User>(`/users/${id}`);
}
async createUser(user: Omit<User, 'id'>): Promise<User> {
return this.api.post<User, Omit<User, 'id'>>('/users', user);
}
}
// libs/data-access/src/index.ts
export * from './lib/api-client';
export * from './lib/users-service';Using in apps:
// apps/web/src/services/index.ts
import { ApiClient, UsersService } from '@my-workspace/data-access';
const apiClient = new ApiClient(process.env.NX_API_URL || 'http://localhost:3333');
export const usersService = new UsersService(apiClient);
// apps/web/src/app/users-page.tsx
import { useEffect, useState } from 'react';
import { usersService } from '../services';
import { User } from '@my-workspace/data-access';
export function UsersPage() {
const [users, setUsers] = useState<User[]>([]);
const [loading, setLoading] = useState(true);
useEffect(() => {
usersService.getUsers()
.then(setUsers)
.finally(() => setLoading(false));
}, []);
if (loading) return <div>Loading...</div>;
return (
<div>
<h1>Users</h1>
{users.map(user => (
<div key={user.id}>
{user.name} - {user.email}
</div>
))}
</div>
);
}
Turborepo: Speed and Simplicity
Turborepo, created by Vercel, focuses on simplicity and extreme performance. It's lighter than Nx but incredibly efficient for JavaScript/TypeScript monorepos.
Turborepo Features
- Zero config cache: Distributed cache with zero configuration
- Incremental builds: Only rebuilds what changed
- Remote caching: Share cache across the entire team
- Lean: Minimalist philosophy, leverages existing tools
- Vercel integration: Native integration with Vercel
Setting Up a Turborepo Monorepo
# Create new monorepo
npx create-turbo@latest
cd my-turborepo
# Generated structure:
# apps/
# web/ # Next.js app
# docs/ # Next.js docs site
# packages/
# ui/ # Shared UI components
# eslint-config/
# tsconfig/
# turbo.json
# package.jsonturbo.json Configuration
{
"$schema": "https://turbo.build/schema.json",
"globalDependencies": [".env"],
"pipeline": {
"build": {
"dependsOn": ["^build"],
"outputs": [".next/**", "dist/**"]
},
"test": {
"dependsOn": ["build"],
"outputs": ["coverage/**"],
"cache": true
},
"lint": {
"cache": true
},
"dev": {
"cache": false,
"persistent": true
}
}
}Workspace Package.json
{
"name": "my-turborepo",
"private": true,
"workspaces": ["apps/*", "packages/*"],
"scripts": {
"build": "turbo run build",
"dev": "turbo run dev",
"test": "turbo run test",
"lint": "turbo run lint"
},
"devDependencies": {
"turbo": "latest"
}
}Shared UI Package
// packages/ui/src/Button.tsx
import * as React from 'react';
export interface ButtonProps {
children: React.ReactNode;
onClick?: () => void;
variant?: 'primary' | 'secondary';
}
export function Button({ children, onClick, variant = 'primary' }: ButtonProps) {
return (
<button
onClick={onClick}
className={`button button--${variant}`}
>
{children}
</button>
);
}
// packages/ui/package.json
{
"name": "@repo/ui",
"version": "0.0.0",
"main": "./src/index.tsx",
"types": "./src/index.tsx",
"license": "MIT",
"scripts": {
"lint": "eslint src/",
"test": "jest"
},
"devDependencies": {
"@repo/eslint-config": "*",
"@repo/typescript-config": "*",
"@types/react": "^18.2.0",
"react": "^18.2.0"
},
"peerDependencies": {
"react": "^18.2.0"
}
}
// packages/ui/src/index.tsx
export * from './Button';
export * from './Input';
export * from './Card';
Remote Caching with Vercel
# Connect to Vercel Remote Cache
npx turbo login
# Link to project
npx turbo link
# Now all builds are cached in the cloud
# The entire team benefits from shared cache// turbo.json with remote cache
{
"$schema": "https://turbo.build/schema.json",
"remoteCache": {
"signature": true
},
"pipeline": {
"build": {
"dependsOn": ["^build"],
"outputs": [".next/**", "dist/**"]
}
}
}Nx vs Turborepo: When to Use Each
Use Nx when:
- Polyglot projects: Need support for multiple languages (TypeScript, Go, Rust, Python)
- Enterprise scale: Hundreds of projects, multiple teams
- Deep customization: Need custom generators, specific plugins
- Complex dependency graph: Want deep visualization and analysis of dependencies
- Conformance rules: Need to ensure standards across entire organization
Use Turborepo when:
- JavaScript/TypeScript only: Total focus on JS ecosystem
- Simplicity: Want minimal configuration, leverage existing tools
- Vercel ecosystem: Use Next.js and deploy on Vercel
- Pure performance: Prioritize absolute build speed
- Small/medium team: Don't need all the complexity of Nx
// Performance comparison (medium project with 20 apps)
const benchmarks = {
nx: {
coldBuild: '45s',
cachedBuild: '2.1s',
affectedBuild: '8.3s',
features: ['affected', 'graph', 'plugins', 'generators']
},
turborepo: {
coldBuild: '38s',
cachedBuild: '1.8s',
affectedBuild: '7.1s',
features: ['cache', 'pipeline', 'remote-cache']
}
};
console.table(benchmarks);
// Both are extremely fast, choose based on needed features
Code Organization Strategies
Boundary Rules (Nx)
// .eslintrc.json - Enforces boundaries between modules
{
"overrides": [
{
"files": ["*.ts"],
"rules": {
"@nx/enforce-module-boundaries": [
"error",
{
"allow": [],
"depConstraints": [
{
"sourceTag": "type:app",
"onlyDependOnLibsWithTags": ["type:feature", "type:ui", "type:util"]
},
{
"sourceTag": "type:feature",
"onlyDependOnLibsWithTags": ["type:ui", "type:util", "type:data-access"]
},
{
"sourceTag": "type:ui",
"onlyDependOnLibsWithTags": ["type:util"]
}
]
}
]
}
}
]
}Tagging System
// libs/shared-ui/project.json
{
"name": "shared-ui",
"tags": ["type:ui", "scope:shared"]
}
// libs/web-feature-auth/project.json
{
"name": "web-feature-auth",
"tags": ["type:feature", "scope:web"]
}
// libs/data-access/project.json
{
"name": "data-access",
"tags": ["type:data-access", "scope:shared"]
}Code Generators (Nx)
// tools/generators/component/index.ts
import { Tree, formatFiles, installPackagesTask } from '@nx/devkit';
export default async function (tree: Tree, schema: any) {
const projectRoot = `libs/${schema.project}/src/lib`;
tree.write(
`${projectRoot}/${schema.name}.tsx`,
`import React from 'react';
export interface ${schema.name}Props {
children?: React.ReactNode;
}
export function ${schema.name}({ children }: ${schema.name}Props) {
return (
<div>
<h1>${schema.name}</h1>
{children}
</div>
);
}
`
);
tree.write(
`${projectRoot}/${schema.name}.spec.tsx`,
`import { render } from '@testing-library/react';
import { ${schema.name} } from './${schema.name}';
describe('${schema.name}', () => {
it('should render successfully', () => {
const { baseElement } = render(<${schema.name} />);
expect(baseElement).toBeTruthy();
});
});
`
);
await formatFiles(tree);
return () => {
installPackagesTask(tree);
};
}
// Usage:
// nx g @my-workspace/tools:component MyComponent --project=shared-ui
Optimized CI/CD for Monorepos
GitHub Actions with Nx
# .github/workflows/ci.yml
name: CI
on:
push:
branches: [main]
pull_request:
jobs:
main:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
with:
fetch-depth: 0
- uses: actions/setup-node@v3
with:
node-version: 18
cache: 'npm'
- run: npm ci
- uses: nrwl/nx-set-shas@v3
- run: npx nx affected -t lint test build --parallel=3
env:
NX_CLOUD_ACCESS_TOKEN: ${{ secrets.NX_CLOUD_ACCESS_TOKEN }}GitHub Actions with Turborepo
# .github/workflows/ci.yml
name: CI
on:
push:
branches: [main]
pull_request:
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: actions/setup-node@v3
with:
node-version: 18
cache: 'npm'
- run: npm ci
- run: npx turbo run build test lint
env:
TURBO_TOKEN: ${{ secrets.TURBO_TOKEN }}
TURBO_TEAM: ${{ secrets.TURBO_TEAM }}Monorepo Best Practices 2025
- Use workspace protocol for internal dependencies
{
"dependencies": {
"@my-workspace/shared-ui": "workspace:*"
}
}- Version shared libraries semantically
nx release version --skip-publish
nx release publish- Configure paths in tsconfig.base.json
{
"compilerOptions": {
"paths": {
"@my-workspace/shared-ui": ["libs/shared-ui/src/index.ts"],
"@my-workspace/*": ["libs/*/src/index.ts"]
}
}
}- Use conventional commits for automatic changelogs
git commit -m "feat(shared-ui): add new Button variant"
git commit -m "fix(api): resolve authentication bug"- Implement code owners
# CODEOWNERS
/libs/shared-ui/ @frontend-team
/apps/api/ @backend-team
/libs/data-access/ @full-stack-teamThe Future of Monorepos
In 2025, monorepos have consolidated as the preferred way to organize code in companies of all sizes. The tools are mature, the community is strong, and the benefits are clear.
What's coming:
- AI-powered code generation: Generators that understand your pattern and suggest code
- Even more performance: Tools written in Rust/Go getting even faster
- Cloud development environments: Workspaces running in the cloud with shared cache
- Cross-language monorepos: Even better support for polyglot projects
- Automated dependency updates: AI that safely updates dependencies
Monorepos aren't a silver bullet, but for most organizations with multiple related projects, they're the best choice in 2025. The question is no longer "if", but "when" to make the transition.
If you want to continue deepening modern development tools and practices, I recommend going back to the beginning of this series: Software Development Market in 2025: Trends, Salaries, and High-Demand Skills where you'll get a complete view of the current market.
Let's go! 🦅
📚 Want to Deepen Your JavaScript Knowledge?
This article covered monorepos and project architecture, but there's much more to explore in modern development.
Developers who invest in solid, structured knowledge tend to have more opportunities in the market.
Complete Study Material
If you want to master JavaScript from basics to advanced, I've prepared a complete guide:
Investment options:
- $4.90 (single payment)
👉 Learn About JavaScript Guide
💡 Material updated with industry best practices

