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