Nx Monorepo: Modulárny produkt bez bolesti hlavy
Nx Monorepo: Modulárny produkt bez bolesti hlavy
Predstavte si tento scenár: Máte produkt s 3 modulmi - Testing, Automation a Robotics. Zákazník A chce všetky tri. Zákazník B len Testing a Automation. Zákazník C len Robotics.
Tradičné riešenia:
Separate branches
Feature flags
Multiple repos
Copy-paste
Moje riešenie: Nx Monorepo + Module Federation
A musím povedať: Toto je ideálne riešenie.
Môj príbeh: Qaron platform
Pracujem na Qaron.tech - modulárnej platforme s troma hlavnými produktmi:
Qaron Ecosystem:
├── Testing Module → Test management
├── Automation Module → Test automation pre non-tech users
└── Robotics Module → RPA automatizáciaBusiness requirement: Zákazník si vyberá len moduly, ktoré potrebuje.
Technical challenge: Ako to spraviť bez maintenance hell?
Problém: Separate branches approach
Pôvodný návrh (ktorý som odmietol):
git branches:
├── main → All modules
├── customer-a-full → Testing + Automation + Robotics
├── customer-b-testing-auto → Testing + Automation
├── customer-c-robotics → Robotics only
└── customer-d-testing → Testing only
Problémy:
❌ Bug fix = 4× commits (every branch)
❌ Feature = merge conflicts hell
❌ Deployment chaos
❌ Version management nightmare
❌ Testing each combination manuallyPríklad bolesti:
# Bug fix in shared auth library
git checkout main
git commit -m "Fix: Auth token refresh"
# Now merge to EVERY customer branch
git checkout customer-a-full
git merge main # Conflict!
git checkout customer-b-testing-auto
git merge main # Conflict!
git checkout customer-c-robotics
git merge main # Conflict!
# ...repeat for multiple customers
# Hours wasted on merging
# High risk of missing a branchRiešenie: Nx Monorepo
Architektúra:
qaron-monorepo/
├── apps/
│ ├── shell/ → Main entry point (host)
│ ├── testing-app/ → Testing module (remote)
│ ├── automation-app/ → Automation module (remote)
│ └── robotics-app/ → Robotics module (remote)
├── libs/
│ ├── shared/
│ │ ├── ui/ → Common UI components
│ │ ├── auth/ → Authentication logic
│ │ ├── api-client/ → gRPC/REST clients
│ │ └── utils/ → Utility functions
│ ├── testing/
│ │ ├── feature-test-cases/ → Test case management
│ │ ├── feature-execution/ → Test execution
│ │ └── data-access/ → Testing API
│ ├── automation/
│ │ ├── feature-scripts/ → Automation scripts
│ │ ├── feature-scheduler/ → Script scheduler
│ │ └── data-access/ → Automation API
│ └── robotics/
│ ├── feature-queues/ → Queue management
│ ├── feature-processes/ → Process execution
│ └── data-access/ → Robotics API
└── nx.jsonDeployment configuration example:
// apps/shell/webpack.config.js
module.exports = {
plugins: [
new ModuleFederationPlugin({
name: 'shell',
remotes: {
testing: getRemoteUrl('testing'),
automation: getRemoteUrl('automation'),
robotics: getRemoteUrl('robotics')
},
shared: {
'@angular/core': { singleton: true, strictVersion: true },
'@angular/common': { singleton: true, strictVersion: true },
'rxjs': { singleton: true },
}
})
]
};
function getRemoteUrl(module: string): string {
const customerId = process.env.CUSTOMER_ID;
const config = getCustomerConfig(customerId);
// Return URL only if customer has access to module
return config.modules.includes(module)
? `${module}@https://cdn.example.com/${module}/remoteEntry.js`
: null;
}
// Example customer configs structure
const configs = {
'customer-a': {
modules: ['testing', 'automation', 'robotics'] // Full package
},
'customer-b': {
modules: ['testing', 'automation'] // Partial package
},
'customer-c': {
modules: ['robotics'] // Single module
}
};Výsledok:
# One codebase
# One bug fix = one commit
git commit -m "Fix: Auth token refresh"
# Deploy once
npm run build:all
# Each customer automatically gets correct modules
# based on their configPrečo Nx Monorepo vyhralo
1. Code sharing without pain
Pred Nx (multiple repos):
// ❌ BAD: Duplicated auth logic
// Repo 1: testing-app
export class AuthService {
login(username, password) { /* ... */ }
refresh() { /* ... */ }
}
// Repo 2: automation-app
export class AuthService {
login(username, password) { /* ... */ }
refresh() { /* ... */ }
}
// Repo 3: robotics-app
export class AuthService {
login(username, password) { /* ... */ }
refresh() { /* ... */ }
}
// Bug fix = 3× commits + 3× reviews + 3× deploysS Nx (monorepo):
// ✅ GOOD: Shared library
// libs/shared/auth/src/lib/auth.service.ts
@Injectable()
export class AuthService {
constructor(
private http: HttpClient,
private tokenStorage: TokenStorageService
) {}
login(username: string, password: string): Observable<AuthResponse> {
return this.http.post<AuthResponse>('/api/auth/login', {
username,
password
}).pipe(
tap(response => this.tokenStorage.saveToken(response.token))
);
}
refresh(): Observable<AuthResponse> {
return this.http.post<AuthResponse>('/api/auth/refresh', {
refreshToken: this.tokenStorage.getRefreshToken()
});
}
}
// Usage in ALL apps
import { AuthService } from '@qaron/shared/auth';
// Bug fix = 1 commit, auto-propagates to all apps2. Dependency management
// ❌ BAD: Multiple repos = version hell
testing-app/package.json: "@angular/core": "15.0.0"
automation-app/package.json: "@angular/core": "14.2.0" ← Conflict!
robotics-app/package.json: "@angular/core": "15.1.0" ← Different!
// ✅ GOOD: Nx monorepo = one version
qaron-monorepo/package.json: "@angular/core": "17.0.0"3. Atomic changes
# ❌ BAD: Multiple repos
# Change API contract = 4 separate PRs
# PR 1: Update API definition (shared-types repo)
# PR 2: Update testing-app
# PR 3: Update automation-app
# PR 4: Update robotics-app
# Must merge in correct order
# Risk: incompatible versions deployed# ✅ GOOD: Nx monorepo
# Change API contract = 1 PR
git commit -m "feat: Add pagination to test cases API"
# Changes:
# libs/shared/api-client/
# libs/testing/data-access/
# apps/testing-app/
# apps/automation-app/ (if uses test cases)
# TypeScript compiler catches ALL breaking changes
# CI tests ALL affected apps
# Deploy atomically4. Build optimization
Nx affected commands:
# Only build what changed
nx affected:build --base=main
# Example: Changed auth library
Affected apps: testing-app, automation-app, robotics-app
Not affected: shell (no changes)
# Nx builds only affected apps
# CI time: 15 min → 5 min (3× faster)Nx cache:
# Developer A builds testing-app
nx build testing-app
# Build time: 2 min
# Developer B (5 min later) builds testing-app
nx build testing-app
# Nx cache HIT!
# Build time: 2 seconds (60× faster)5. Module Federation: Dynamic loading
Problem: Zákazník platí len za Testing module, ale dostane full bundle?
Solution: Module Federation lazy loading
// Shell app - dynamically loads only purchased modules
@Component({
selector: 'app-root',
template: `
<app-header></app-header>
<app-navigation [modules]="availableModules"></app-navigation>
<router-outlet></router-outlet>
`
})
export class AppComponent {
availableModules = [];
ngOnInit() {
// Load only modules customer has access to
this.loadAvailableModules();
}
private async loadAvailableModules() {
const config = await this.configService.getCustomerConfig();
if (config.modules.includes('testing')) {
this.availableModules.push({
name: 'Testing',
route: 'testing',
icon: 'test-tube'
});
}
if (config.modules.includes('automation')) {
this.availableModules.push({
name: 'Automation',
route: 'automation',
icon: 'robot'
});
}
if (config.modules.includes('robotics')) {
this.availableModules.push({
name: 'Robotics',
route: 'robotics',
icon: 'cog'
});
}
}
}
// Routing - lazy load modules
const routes: Routes = [
{
path: 'testing',
loadChildren: () =>
import('testing/Module').then(m => m.TestingModule),
canActivate: [ModuleAccessGuard]
},
{
path: 'automation',
loadChildren: () =>
import('automation/Module').then(m => m.AutomationModule),
canActivate: [ModuleAccessGuard]
},
{
path: 'robotics',
loadChildren: () =>
import('robotics/Module').then(m => m.RoboticsModule),
canActivate: [ModuleAccessGuard]
}
];
// Guard - check access
@Injectable()
export class ModuleAccessGuard implements CanActivate {
constructor(private configService: ConfigService) {}
canActivate(route: ActivatedRouteSnapshot): boolean {
const moduleName = route.url[0].path;
const config = this.configService.getCustomerConfig();
return config.modules.includes(moduleName);
}
}Výsledok (príklad):
Full package zákazník:
Bundle: All modules loaded on demand
Initial load: Shell + shared libraries
Partial package zákazník:
Bundle: Only purchased modules available
Initial load: Shell + shared libraries
Lazy load: Only testing + automation modules
Single module zákazník:
Bundle: Only one module available
Initial load: Shell + shared libraries
Lazy load: Only robotics moduleReal-world príklad: Migration zo starého na nový
Scenár: Máme starú Angular aplikáciu, chceme postupne migrovať na novú verziu.
Tradičné riešenie:
# Big bang migration
git checkout -b angular-migration
# Rewrite EVERYTHING
# Months of work
# High risk
# Deployment: all or nothingNx + Module Federation riešenie:
Fáza 1: Shell (nový) + Old app (wrapped)
qaron-monorepo/
├── apps/
│ ├── shell/ → New Angular (new)
│ └── legacy-wrapper/ → Old Angular (wrapped as remote)
Fáza 2: Migrate Testing module
qaron-monorepo/
├── apps/
│ ├── shell/ → New
│ ├── testing-app/ → New (migrated!)
│ ├── automation-legacy/ → Old
│ └── robotics-legacy/ → Old
Fáza 3: Migrate Automation module
qaron-monorepo/
├── apps/
│ ├── shell/ → New
│ ├── testing-app/ → New
│ ├── automation-app/ → New (migrated!)
│ └── robotics-legacy/ → Old
Fáza 4: Complete migration
qaron-monorepo/
├── apps/
│ ├── shell/ → New
│ ├── testing-app/ → New
│ ├── automation-app/ → New
│ └── robotics-app/ → New (complete!)Implementation:
// Shell routing - mix old and new
const routes: Routes = [
{
path: 'testing',
loadChildren: () =>
import('testing/Module').then(m => m.TestingModule),
// ✅ NEW: Migrated module
},
{
path: 'automation',
loadChildren: () =>
import('automation-legacy/Module').then(m => m.LegacyModule),
// ⚠️ OLD: Legacy wrapped module
},
{
path: 'robotics',
component: LegacyWrapperComponent,
data: {
legacyUrl: 'https://old.example.com/robotics'
}
// 🔴 LEGACY: iframe wrapper (temporary)
}
];
// Legacy wrapper component
@Component({
template: `
<iframe
[src]="legacyUrl | safe"
style="width: 100%; height: 100vh; border: none">
</iframe>
`
})
export class LegacyWrapperComponent {
@Input() legacyUrl: string;
}Benefits:
✅ Incremental migration (module by module)
✅ Zero downtime
✅ Test each module independently
✅ Rollback easy (switch back to old module)
✅ Team can work on different modules in parallel
✅ Users don't notice the migrationSpojenie ekosystémov: Angular + React + Vue
Another use case: Automation module potrebuje React library pre script editor.
Nx makes it easy:
// Nx workspace supports multiple frameworks
qaron-monorepo/
├── apps/
│ ├── shell/ → Angular
│ ├── testing-app/ → Angular
│ ├── automation-app/ → Angular
│ └── robotics-app/ → Angular
├── libs/
│ ├── shared/
│ │ └── ui/ → Angular components
│ ├── script-editor/ → React component!
│ └── chart-lib/ → Vue component!
// Angular app can use React component via Module FederationImplementation:
// libs/script-editor/webpack.config.js (React)
module.exports = {
plugins: [
new ModuleFederationPlugin({
name: 'scriptEditor',
filename: 'remoteEntry.js',
exposes: {
'./Editor': './src/components/Editor.tsx'
},
shared: {
react: { singleton: true },
'react-dom': { singleton: true }
}
})
]
};
// apps/automation-app (Angular) uses React component
import { Component, ElementRef, ViewChild } from '@angular/core';
import { loadRemoteModule } from '@nx/angular/mf';
@Component({
selector: 'app-script-editor',
template: '<div #editorHost></div>'
})
export class ScriptEditorComponent implements OnInit {
@ViewChild('editorHost', { static: true })
editorHost: ElementRef;
async ngOnInit() {
// Load React component from remote
const { Editor } = await loadRemoteModule('scriptEditor', './Editor');
// Render React component in Angular
const root = ReactDOM.createRoot(this.editorHost.nativeElement);
root.render(<Editor
code={this.script}
onChange={(code) => this.onScriptChange(code)}
/>);
}
}Výsledok:
- Angular apps môžu používať React libraries
- React apps môžu používať Angular components
- Vue apps môžu používať oboje
- Best tool for the job!
Nx commands ktoré používam denne
1. Affected builds
# Build only what changed since main
nx affected:build --base=main --head=HEAD
# Real example from my workflow
git checkout -b feature/test-case-filtering
# ... make changes in libs/testing/feature-test-cases
nx affected:build --base=main
# Nx detects: testing-app affected
# Builds only: testing-app (not automation-app, robotics-app)
# Time saved: 10 min → 3 min2. Dependency graph
# Visualize project dependencies
nx graph
# Opens browser with interactive graph showing:
# - Which apps depend on which libs
# - Circular dependencies (bad!)
# - Impact analysisExample output:
shell
├─→ @qaron/shared/ui
├─→ @qaron/shared/auth
└─→ @qaron/shared/api-client
testing-app
├─→ @qaron/shared/ui
├─→ @qaron/shared/auth
├─→ @qaron/shared/api-client
├─→ @qaron/testing/feature-test-cases
├─→ @qaron/testing/feature-execution
└─→ @qaron/testing/data-access
└─→ @qaron/shared/api-client (reused!)
automation-app
├─→ @qaron/shared/ui
├─→ @qaron/shared/auth
└─→ ...3. Lint & test affected
# Lint only affected projects
nx affected:lint
# Test only affected projects
nx affected:test
# E2E test only affected apps
nx affected:e2e4. Run many
# Build all apps
nx run-many --target=build --all
# Build specific apps
nx run-many --target=build --projects=testing-app,automation-app
# Parallel execution (faster)
nx run-many --target=build --all --parallel=35. Workspace generators
# Generate new lib
nx g @nx/angular:lib feature-notifications --directory=libs/shared
# Generate new app
nx g @nx/angular:app portal-app
# Generate component in lib
nx g @nx/angular:component button --project=shared-uiBest practices z mojej praxe
1. Library organization
// ✅ GOOD: Granular libraries
libs/
├── shared/
│ ├── ui/ → Reusable UI components
│ ├── auth/ → Authentication logic
│ ├── api-client/ → API communication
│ └── utils/ → Utility functions
├── testing/
│ ├── feature-test-cases/ → Feature-specific
│ ├── feature-execution/ → Feature-specific
│ ├── data-access/ → Data layer
│ └── ui/ → Testing-specific UI
// ❌ BAD: Monolithic libraries
libs/
├── shared/ → Everything in one lib (unmaintainable)
└── testing/ → All testing code in one lib2. Access restrictions
// nx.json - enforce boundaries
{
"namedInputs": {
"default": ["{projectRoot}/**/*"]
},
"targetDefaults": {
"build": {
"dependsOn": ["^build"]
}
},
"generators": {
"@nx/angular:library": {
"tags": ""
}
}
}
// libs/testing/feature-test-cases/project.json
{
"tags": ["scope:testing", "type:feature"]
}
// Enforce: automation cannot import testing features
// .eslintrc.json
{
"rules": {
"@nx/enforce-module-boundaries": [
"error",
{
"allow": [],
"depConstraints": [
{
"sourceTag": "scope:automation",
"onlyDependOnLibsWithTags": [
"scope:automation",
"scope:shared"
]
}
]
}
]
}
}3. Shared libraries pattern
// Pattern: barrel exports
// libs/shared/ui/src/index.ts
export * from './lib/button/button.component';
export * from './lib/input/input.component';
export * from './lib/modal/modal.component';
// Apps import from barrel
import { Button, Input, Modal } from '@qaron/shared/ui';
// NOT:
import { Button } from '@qaron/shared/ui/button'; // ❌ Deep imports4. Module Federation configuration
// Shared dependencies configuration
shared: {
'@angular/core': {
singleton: true,
strictVersion: true,
requiredVersion: '^17.0.0'
},
'@angular/common': {
singleton: true,
strictVersion: true
},
'rxjs': {
singleton: true,
strictVersion: false // Allow minor versions
},
'@qaron/shared/auth': {
singleton: true,
strictVersion: true,
requiredVersion: 'auto' // Use version from package.json
}
}5. CI/CD optimization
# .github/workflows/ci.yml
name: CI
on: [push, pull_request]
jobs:
main:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
with:
fetch-depth: 0 # Important for Nx affected
- name: Setup Node
uses: actions/setup-node@v3
with:
node-version: 18
cache: 'npm'
- name: Install dependencies
run: npm ci
- name: Nx affected
run: |
npx nx affected:lint --base=origin/main
npx nx affected:test --base=origin/main --coverage
npx nx affected:build --base=origin/main
- name: Upload coverage
uses: codecov/codecov-action@v3Comparison: Nx Monorepo vs Alternatives
Nx vs Separate Repos
| Kritérium | Separate Repos | Nx Monorepo | Winner |
|---|---|---|---|
| Code sharing | Difficult (npm packages) | Easy (imports) | 🏆 Nx |
| Dependency mgmt | Version conflicts | Single version | 🏆 Nx |
| Atomic changes | Multiple PRs | Single PR | 🏆 Nx |
| CI/CD time | Build all repos | Build affected only | 🏆 Nx |
| Team autonomy | High | Medium | 🏆 Separate |
| Repository size | Small | Large | 🏆 Separate |
Nx vs Feature Flags
| Kritérium | Feature Flags | Nx Monorepo | Winner |
|---|---|---|---|
| Code complexity | High (if/else everywhere) | Low (separate modules) | 🏆 Nx |
| Bundle size | Large (all code) | Small (lazy load) | 🏆 Nx |
| Testing | Complex (all combinations) | Simple (module isolation) | 🏆 Nx |
| Runtime toggle | Yes | No | 🏆 Flags |
| Maintenance | Difficult | Easy | 🏆 Nx |
Nx vs Lerna
| Kritérium | Lerna | Nx | Winner |
|---|---|---|---|
| Build speed | Slow | Fast (cache + affected) | 🏆 Nx |
| Dependency graph | No | Yes | 🏆 Nx |
| Affected detection | No | Yes | 🏆 Nx |
| Generators | No | Yes | 🏆 Nx |
| Module Federation | No support | Built-in | 🏆 Nx |
Kedy NEPOUŽIŤ Nx Monorepo
Buďme realistickí. Nx nie je silver bullet.
1. Malé projekty
Projekt: Simple landing page
Team: 1-2 developers
Complexity: Low
Nx overhead: Not worth it
Better: Simple Create React App2. Rôzne release cadences
Projekt: API (monthly releases) + Frontend (daily releases)
Problem: Monorepo wants atomic releases
Better: Separate repos with clear contracts3. Different teams with no code sharing
Team A: E-commerce platform (React)
Team B: Admin dashboard (Angular)
Team C: Mobile app (React Native)
No shared code, no shared dependencies
Better: Separate repos4. Very strict access control
Team A: Cannot see Team B's code (legal/compliance)
Problem: Monorepo = shared access
Better: Separate repos with strict permissionsZáver: Nx pre modulárne produkty
Na základe skúseností s Nx na Qaron platforme môžem povedať:
Výhody ktoré nás presvedčili:
- 🚀 Modulárny produkt bez branch hell
- 💪 Code sharing cez shared libraries
- 🔄 Atomic changes cez celý system
- 📦 Dynamic loading len kúpených modulov
- 🎯 Build optimization (affected + cache)
- 🌐 Multi-framework support (Angular + React)
Kedy ho používame:
- Modulárny produkt (zákazník platí za moduly)
- Potreba zdieľať kód medzi aplikáciami
- Multiple apps v jednom domaine
- Incremental migration (starý → nový)
Kedy ho NEPOUŽÍVAME:
- Malé projekty (1-2 apps)
- Žiadne code sharing
- Different release cadences
- Strict access control requirements
Best pattern pre nás:
Nx Monorepo
+ Module Federation (dynamic loading)
+ Shared libraries (DRY)
+ Customer configs (modularity)
= Modulárny produkt without painOtázka nie je "Mám použiť monorepo?"
Otázka je "Ako dlho ešte chcem mergovať separate branches?"
Článok napísal Solution Architect, ktorý implementoval Nx monorepo architektúru pre modulárny enterprise produkt s viacerými aplikáciami a zdieľanými knižnicami.
Stack: Nx, Angular, Module Federation, TypeScript, Spring Boot backend, gRPC communication.
Check out Qaron.tech - modulárna platforma postavená na moderných princípoch.
Začnite s Nx dnes. Váš future self vám poďakuje.