Architecting Scalable Frontend Monorepos
Monorepos have become the standard for large-scale frontend development, but scaling them effectively requires careful architectural decisions. Having worked with monorepos supporting 50+ developers and multiple products, I'll share the patterns that actually work in production.
Workspace Structure and Dependency Management
The foundation of a scalable monorepo is a well-designed workspace structure:
monorepo/
├── apps/
│ ├── web-app/ # Main customer-facing app
│ ├── admin-dashboard/ # Internal admin tool
│ └── marketing-site/ # Public marketing site
├── packages/
│ ├── ui/ # Shared component library
│ ├── utils/ # Utility functions
│ ├── hooks/ # Custom React hooks
│ ├── types/ # Shared TypeScript types
│ └── config/ # Build and lint configurations
├── tools/
│ ├── scripts/ # Build and deployment scripts
│ └── generators/ # Code generation templates
└── package.json # Root package.json
// Root package.json for workspace configuration
{
"name": "company-monorepo",
"private": true,
"workspaces": ["apps/*", "packages/*"],
"scripts": {
"build": "turbo run build",
"dev": "turbo run dev --parallel",
"test": "turbo run test",
"lint": "turbo run lint"
},
"devDependencies": {
"turbo": "^1.8.0",
"typescript": "^4.9.0"
}
}
Advanced Turborepo Configuration
Leverage Turborepo's powerful caching and task orchestration:
// turbo.json
{
"$schema": "https://turbo.build/schema.json",
"pipeline": {
"build": {
"dependsOn": ["^build"],
"outputs": ["dist/**", "build/**"],
"env": ["NODE_ENV"]
},
"dev": {
"cache": false,
"persistent": true
},
"test": {
"dependsOn": ["build"],
"outputs": [],
"env": ["TEST_ENV"]
},
"lint": {
"dependsOn": ["^build"],
"outputs": []
},
"type-check": {
"dependsOn": ["^build"],
"outputs": []
}
},
"globalEnv": [
"API_URL",
"ENABLE_ANALYTICS"
]
}
// Environment-specific configurations
const getTurboConfig = (environment) => ({
build: {
env: {
NODE_ENV: environment,
...getEnvironmentVars(environment)
}
}
});
Shared Tooling and Code Generation
Create consistent development experiences across the monorepo:
// packages/config/eslint-config-custom/index.js
module.exports = {
extends: [
'eslint:recommended',
'@typescript-eslint/recommended',
'react-app',
'react-app/jest'
],
rules: {
// Custom rules for the entire monorepo
'@typescript-eslint/no-unused-vars': 'error',
'react-hooks/exhaustive-deps': 'error'
},
overrides: [
{
files: ['**/*.test.*'],
rules: {
// Test-specific rules
}
}
]
};
// Plop templates for consistent component generation
export const componentGenerator = {
description: 'Create a new component',
prompts: [
{
type: 'input',
name: 'name',
message: 'Component name:'
},
{
type: 'confirm',
name: 'withTests',
message: 'Include test files?',
default: true
}
],
actions: (data) => {
const actions = [
{
type: 'add',
path: 'packages/ui/src/{{pascalCase name}}/index.ts',
templateFile: 'templates/component/index.ts.hbs'
},
// ... more template actions
];
return data.withTests ? [...actions, testAction] : actions;
}
};
Dependency Graph and Build Optimization
Manage dependencies and build order efficiently:
// tools/scripts/analyze-deps.js
const analyzeDependencies = () => {
const graph = {};
// Build dependency graph
workspaces.forEach(workspace => {
graph[workspace.name] = {
dependencies: getWorkspaceDeps(workspace),
dependents: getWorkspaceDependents(workspace.name)
};
});
return {
findCircularDependencies: () => {
// Implementation to detect circular deps
},
optimizeBuildOrder: () => {
// Topological sort for optimal build order
},
visualize: () => {
// Generate visual dependency graph
}
};
};
// Advanced caching strategy
const cacheStrategy = {
key: (task, source) => {
// Custom cache key based on task and source changes
return `${task}-${hashSource(source)}`;
},
restore: (key) => {
// Intelligent cache restoration
},
shouldCache: (task) => {
// Skip cache for certain tasks or conditions
return !task.includes('dev');
}
};
A well-architected monorepo is a force multiplier for development teams. These patterns, refined through years of scaling frontend architecture, will help you build a foundation that supports rapid iteration, consistent quality, and team autonomy while maintaining architectural integrity.