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]

    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:

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
// ✅ 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
// ✅ 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
// ✅ 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
// ❌ 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
// ❌ 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

    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:

# Only builds what changed
npx nx affected:build

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:

.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
{
    "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
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