Skip to content

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:

eslint.config.mjs (excerpt)
{
  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

apps/petfolio-business/src/app/page.tsx
1
2
3
4
5
6
7
8
9
// ✅ 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';
libs/ui-component-library/src/lib/atoms/Button/Button.tsx
1
2
3
4
5
// ✅ 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';
libs/util-global-theme/src/lib/themes/default.ts
1
2
3
4
// ✅ Styles can use MUI theming
import { createTheme } from '@mui/material/styles';

// ✅ No imports from other workspace libraries

Invalid imports

libs/ui-component-library/src/lib/atoms/Button/Button.tsx
1
2
3
4
5
// ❌ 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"
libs/util-global-theme/src/lib/themes/default.ts
1
2
3
4
5
// ❌ 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 []
apps/petfolio-docs/mkdocs.yml
#  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

    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:

# Only builds what changed
npx nx affected:build

Checking boundaries

Lint command

1
2
3
4
5
6
7
8
# 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

Visualise dependencies

1
2
3
4
5
# 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:

.github/workflows/ci.yml (excerpt)
- name: Lint
  run: npx nx affected:lint --base=origin/main

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
1
2
3
4
5
6
7
{
    "name": "my-new-lib",
    "tags": ["scope:ui-shared"], // Choose appropriate scope
    "targets": {
        /* ... */
    }
}

Then update ESLint config if you need new boundary rules:

eslint.config.mjs
1
2
3
4
5
6
7
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

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.