Module Boundaries¶
Nx enforces strict module boundaries through ESLint rules to maintain a clean architecture and prevent circular dependencies.
Boundary Rules¶
The module boundary system uses tags to control which projects can depend on others.
Tag System¶
graph TD
A[scope:petfolio-business<br/>Main App] --> B[scope:ui-shared<br/>Component Library]
A --> C[scope:styles<br/>Theme Utils]
B --> C
D[scope:docs<br/>Documentation]
style A fill:#4fc3f7
style B fill:#81c784
style C fill:#ffb74d
style D fill:#ba68c8
| Tag | Purpose | Can Import From |
|---|---|---|
scope:petfolio-business |
Main application | scope:ui-shared, scope:styles |
scope:ui-shared |
Shared UI components | scope:styles |
scope:styles |
Theme and styling | (none - leaf node) |
scope:docs |
Documentation | (none - isolated) |
ESLint Configuration¶
Boundaries are enforced in eslint.config.mjs:
{
files: ['**/*.ts', '**/*.tsx', '**/*.js', '**/*.jsx'],
rules: {
'@nx/enforce-module-boundaries': [
'error',
{
enforceBuildableLibDependency: true,
allow: [],
depConstraints: [
{
sourceTag: 'scope:petfolio-business',
onlyDependOnLibsWithTags: ['scope:ui-shared', 'scope:styles']
},
{
sourceTag: 'scope:ui-shared',
onlyDependOnLibsWithTags: ['scope:styles']
},
{
sourceTag: 'scope:styles',
onlyDependOnLibsWithTags: []
},
{
sourceTag: 'scope:docs',
onlyDependOnLibsWithTags: []
}
]
}
]
}
}
Examples¶
✅ Valid Imports¶
// ✅ App can import from ui-shared
import { Button } from '@ui-component-library';
// ✅ App can import from styles
import { ThemeProvider } from '@util-global-theme';
// ✅ App can use Next.js and React
import { useState } from 'react';
import Image from 'next/image';
// ✅ UI components can import from styles
import { useTheme } from '@util-global-theme';
// ✅ UI components can use React and MUI
import { Button as MuiButton } from '@mui/material';
// ✅ Styles can use MUI theming
import { createTheme } from '@mui/material/styles';
// ✅ No imports from other workspace libraries
❌ Invalid Imports¶
// ❌ ERROR: ui-shared cannot import from petfolio-business
import { config } from '@petfolio-business/config';
// ESLint error:
// A project tagged with "scope:ui-shared" can only depend on libs tagged with "scope:styles"
// ❌ ERROR: styles cannot import from ui-shared
import { Button } from '@ui-component-library';
// ESLint error:
// A project tagged with "scope:styles" can only depend on libs tagged with []
# ✅ Docs are isolated - no TypeScript imports allowed
# Documentation is separate from the codebase
Why Module Boundaries?¶
1. Prevent Circular Dependencies¶
graph LR
A[App] --> B[UI Lib]
B -.->|❌ Blocked| A
style A fill:#4fc3f7
style B fill:#81c784
Without boundaries, projects could create circular dependencies that make builds fail.
2. Enforce Architectural Layers¶
graph TD
A[Application Layer<br/>petfolio-business] --> B[Shared UI Layer<br/>ui-component-library]
B --> C[Foundation Layer<br/>util-global-theme]
style A fill:#4fc3f7
style B fill:#81c784
style C fill:#ffb74d
Higher layers can depend on lower layers, but not vice versa.
3. Enable Independent Development¶
Teams can work on libraries without breaking consumers:
- Theme team works on
util-global-theme - Component team works on
ui-component-library - App team works on
petfolio-business
Changes to lower layers don't require changes to higher layers.
4. Improve Build Performance¶
Nx can cache and skip rebuilding unaffected projects:
Checking Boundaries¶
Lint Command¶
# Check all projects
npx nx run-many --target=lint --all
# Check specific project
npx nx lint petfolio-business
# Auto-fix (won't fix boundary violations)
npx nx lint petfolio-business --fix
Visualize Dependencies¶
# Open interactive dependency graph
npx nx graph
# Focus on specific project
npx nx graph --focus=ui-component-library
CI Enforcement¶
The CI pipeline automatically checks boundaries:
Pull requests cannot merge if boundary violations exist.
Adding New Projects¶
When creating a new project, assign appropriate tags:
{
"name": "my-new-lib",
"tags": ["scope:ui-shared"], // Choose appropriate scope
"targets": {
/* ... */
}
}
Then update ESLint config if you need new boundary rules:
depConstraints: [
// ... existing rules ...
{
sourceTag: 'scope:my-new-scope',
onlyDependOnLibsWithTags: ['scope:styles'],
},
];
Common Violations¶
1. Importing from App into Library¶
// ❌ libs/ui-component-library/src/lib/atoms/Button/Button.tsx
import { appConfig } from '@petfolio-business/config';
Solution: Move shared config to a library.
2. Circular Dependencies¶
// ❌ libs/util-global-theme/src/lib/index.ts
import { Button } from '@ui-component-library'; // ui-shared imports from styles
Solution: Restructure to avoid cycles.
3. Using Relative Paths Across Boundaries¶
// ❌ apps/petfolio-business/src/app/page.tsx
import { Button } from '../../../libs/ui-component-library/src/lib/atoms/Button';
Solution: Use path aliases (@ui-component-library).
Best Practices¶
!!! tip "Keep Libraries Focused" Each library should have a single, well-defined purpose.
!!! tip "Prefer Composition Over Inheritance" Build complex components from simple ones rather than creating deep hierarchies.
!!! tip "Export Through Barrel Files" Always export through index.ts to
control the public API.
!!! warning "Don't Bypass Boundaries" Never use eslint-disable to bypass
boundary rules - fix the architecture instead.
Next Steps¶
- Monorepo Structure - Understand the project layout
- Development Workflows - Learn common tasks
- Component Library - Work with shared components