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]
classDef layer-api fill:#4a90d926,stroke:#4a90d9,stroke-width:2px
classDef layer-domain fill:#7ed32126,stroke:#7ed321,stroke-width:2px
classDef layer-infra fill:#f5a6232e,stroke:#f5a623,stroke-width:2px
classDef layer-external fill:#9b9b9b2e,stroke:#9b9b9b,stroke-width:2px
class A layer-api
class B layer-domain
class C layer-infra
class D layer-external
Diagram key
- Blue (API layer): Main application - top-level consumer
- Green (Domain layer): Shared UI component library
- Orange (Infrastructure layer): Theme and styling foundation
- Grey (External): Documentation - isolated, no code dependencies
| 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:
Examples¶
Valid imports¶
| apps/petfolio-business/src/app/page.tsx | |
|---|---|
| libs/ui-component-library/src/lib/atoms/Button/Button.tsx | |
|---|---|
| libs/util-global-theme/src/lib/themes/default.ts | |
|---|---|
Invalid imports¶
| libs/ui-component-library/src/lib/atoms/Button/Button.tsx | |
|---|---|
| libs/util-global-theme/src/lib/themes/default.ts | |
|---|---|
| apps/petfolio-docs/mkdocs.yml | |
|---|---|
Why module boundaries?¶
1. Prevent circular dependencies¶
graph LR
A[App] --> B[UI Lib]
B -.->|❌ Blocked| A
classDef layer-api fill:#4a90d926,stroke:#4a90d9,stroke-width:2px
classDef layer-domain fill:#7ed32126,stroke:#7ed321,stroke-width:2px
class A layer-api
class B layer-domain
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]
classDef layer-api fill:#4a90d926,stroke:#4a90d9,stroke-width:2px
classDef layer-domain fill:#7ed32126,stroke:#7ed321,stroke-width:2px
classDef layer-infra fill:#f5a6232e,stroke:#f5a623,stroke-width:2px
class A layer-api
class B layer-domain
class C layer-infra
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¶
Visualise dependencies¶
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:
| libs/my-new-lib/project.json | |
|---|---|
Then update ESLint config if you need new boundary rules:
| eslint.config.mjs | |
|---|---|
Common violations¶
1. Importing from app into library¶
Solution: Move shared config to a library.
2. Circular dependencies¶
Solution: Restructure to avoid cycles.
3. Using relative paths across boundaries¶
Solution: Use path aliases (@ui-component-library).
Best practices¶
Keep libraries focused
Each library should have a single, well-defined purpose.
Prefer composition over inheritance
Build complex components from simple ones rather than creating deep hierarchies.
Use direct imports with path aliases
Import directly from source files using TypeScript path aliases (e.g. @ui-component-library/atoms/Button/Button) rather than barrel index.ts files. Barrel exports are banned in this project to avoid bundle size and HMR performance issues.
Don't bypass boundaries
Never use eslint-disable to bypass boundary rules - fix the architecture instead.