Skip to content

Dependency Cruiser Configuration ​

Enforcing architectural boundaries in NestJS modular monoliths

Related: Modular Monolith in NestJS | Clean Architecture

Overview ​

Dependency Cruiser is a tool that validates and visualizes dependencies in JavaScript and TypeScript projects. In our NestJS applications, we use it to enforce architectural boundaries and prevent unintended coupling between modules.

Complete Configuration ​

This is the actual .dependency-cruiser.js configuration file used in the Aether backend:

javascript
module.exports = {
  forbidden: [
    {
      name: 'no-nestjs-in-domain',
      comment: 'Domain layer should not depend on NestJS framework',
      severity: 'error',
      from: {
        path: '^src/.*[/\\\\]domain[/\\\\]',
      },
      to: {
        path: '^node_modules/@nestjs/',
      },
    },
    {
      name: 'no-nestjs-in-use-cases',
      comment: 'Use cases should not depend on NestJS framework',
      severity: 'error',
      from: {
        path: '^src/.*[/\\\\]use-cases[/\\\\]',
      },
      to: {
        path: '^node_modules/@nestjs/',
      },
    },
    {
      name: 'domain-isolation',
      comment: 'Domain code should not depend on code outside its own domain folder',
      severity: 'error',
      from: {
        path: '^src/modules/([^/]+)/domain/',
      },
      to: {
        path: '^src/',
        pathNot: ['^src/modules/$1/domain/', '^src/core/types/'],
      },
    },
    {
      name: 'no-cross-module-imports',
      comment: 'Only adapters and main module files can import from other modules',
      severity: 'error',
      from: {
        path: '^src/modules/([^/]+)/',
        pathNot: ['^src/modules/[^/]+/adapters/', '^src/modules/[^/]+/[^/]+\\.module\\.(ts|js)$'],
      },
      to: {
        path: '^src/modules/(?!$1)[^/]+/',
        pathNot: [
          // Allow importing auth guards from any module
          '^src/modules/auth/infrastructure/guards/',
        ],
      },
    },
    {
      name: 'no-shared-imports-outside-adapters',
      comment: 'Only repositories and adapters can access shared infrastructure services',
      severity: 'error',
      from: {
        path: '^src/modules/',
        pathNot: [
          '^src/modules/[^/]+/infrastructure/.*\\.repository\\.(ts|js)$',
          '^src/modules/[^/]+/adapters/',
          '^src/modules/[^/]+/.*\\.integration-spec\\.(ts|js)$',
        ],
      },
      to: {
        path: '^src/shared/infrastructure/',
        pathNot: ['^src/shared/infrastructure/database/database\\.module\\.(ts|js)$'],
      },
    },
    {
      name: 'adapters-only-use-public-services',
      comment: 'Adapters can only depend on public services from other modules',
      severity: 'error',
      from: {
        path: '^src/modules/([^/]+)/adapters/',
      },
      to: {
        path: '^src/modules/(?!$1)[^/]+/',
        pathNot: [
          '^src/modules/[^/]+/public/',                    // Only public/ directory allowed
          '^src/modules/[^/]+/[^/]+\\.module\\.(ts|js)$',  // Module files allowed
        ],
      },
    },
  ],
  options: {
    doNotFollow: {
      path: '^node_modules/(?!@nestjs)',
      dependencyTypes: ['npm-bundled', 'npm-dev', 'npm-optional', 'npm-peer'],
    },
    tsPreCompilationDeps: true,
    tsConfig: {
      fileName: 'tsconfig.json',
    },
    enhancedResolveOptions: {
      exportsFields: ['exports'],
      conditionNames: ['import', 'require', 'node', 'default'],
    },
    reporterOptions: {
      dot: {
        collapsePattern: 'node_modules/[^/]+',
        filters: {
          includeOnly: {
            path: '^src/',
          },
        },
      },
      text: {
        highlightFocused: true,
      },
    },
  },
};

Rule Explanations ​

1. no-nestjs-in-domain ​

Purpose: Keep domain layer framework-agnostic

Prevents: Domain entities and services from importing @nestjs/* packages

Why: Domain logic should be pure TypeScript, enabling testing without framework dependencies and potential future framework changes

2. no-nestjs-in-use-cases ​

Purpose: Keep use cases framework-agnostic

Prevents: Use case files from importing @nestjs/* packages

Why: Use cases represent business logic that should be independent of the delivery mechanism (HTTP, GraphQL, CLI, etc.)

3. domain-isolation ​

Purpose: Enforce domain encapsulation

Prevents: Domain code from importing anything outside its own domain folder (except shared core types)

Why: Each domain should be self-contained and not depend on other domains or infrastructure concerns

4. no-cross-module-imports ​

Purpose: Enforce module boundaries through adapters

Prevents: Any file (except adapters and module files) from importing from other modules

Allows:

  • Adapter files can import from other modules
  • Module files can import other modules (for NestJS imports array)
  • Auth guards can be imported from anywhere (cross-cutting concern)

Why: This is the core rule that enforces the adapter pattern for inter-module communication

5. no-shared-imports-outside-adapters ​

Purpose: Limit access to shared infrastructure

Prevents: Most module code from directly accessing shared infrastructure services

Allows:

  • Repositories can access shared infrastructure (e.g., database connections)
  • Adapters can access shared infrastructure
  • Integration tests can access shared infrastructure
  • Any code can import the database module

Why: Infrastructure concerns should be handled at the edges (repositories/adapters), not in domain or use case layers

6. adapters-only-use-public-services ​

Purpose: Enforce public API boundaries between modules

Prevents: Adapters from reaching into other modules' internals

Allows:

  • Imports from other modules' /public directories
  • Imports of module files (for NestJS module imports)

Why: This ensures modules only expose what they intend to be public, preventing accidental coupling to internal implementation details

Usage ​

Installation ​

bash
npm install --save-dev dependency-cruiser

Package.json Scripts ​

json
{
  "scripts": {
    "lint:dependencies": "depcruise src",
    "lint:dependencies:graph": "depcruise src --output-type dot | dot -T svg > dependency-graph.svg",
    "lint:dependencies:watch": "depcruise src --watch"
  }
}

Running Validation ​

bash
# Check for violations
npm run lint:dependencies

# Generate visual dependency graph
npm run lint:dependencies:graph

# Watch mode during development
npm run lint:dependencies:watch

CI/CD Integration ​

Add to your CI pipeline to prevent architectural violations from being merged:

yaml
# .github/workflows/ci.yml
- name: Validate Dependencies
  run: npm run lint:dependencies

Example Violations and Fixes ​

❌ Violation: Use Case Importing from Another Module ​

typescript
// modules/task/use-cases/create-task.use-case.ts
import { OrganizationService } from '../../organization/domain/organization.service';
// ERROR: no-cross-module-imports

Fix: Create an adapter and use dependency injection

typescript
// modules/task/use-cases/create-task.use-case.ts
export class CreateTaskUseCase {
  constructor(
    @Inject('ORGANIZATION_PROVIDER')
    private readonly organizationProvider: OrganizationProvider,
  ) {}
}

❌ Violation: Domain Using NestJS ​

typescript
// modules/order/domain/order.service.ts
import { Injectable } from '@nestjs/common';
// ERROR: no-nestjs-in-domain

Fix: Remove NestJS decorators from domain layer

typescript
// modules/order/domain/order.service.ts
export class OrderService {
  // Pure TypeScript class, no framework dependencies
}

❌ Violation: Adapter Accessing Internal Module Code ​

typescript
// modules/task/adapters/organization.adapter.ts
import { OrganizationRepository } from '../../organization/infrastructure/organization.repository';
// ERROR: adapters-only-use-public-services

Fix: Use the public API instead

typescript
// modules/task/adapters/organization.adapter.ts
import { OrganizationPublicService } from '../../organization/public/organization.public-service.interface';

Visualizing Dependencies ​

Generate a dependency graph to visualize your module structure:

bash
npm run lint:dependencies:graph

This creates a dependency-graph.svg file showing:

  • Module relationships
  • Dependency directions
  • Violations (highlighted in red)

Next Steps ​