Appearance
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
importsarray) - 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'
/publicdirectories - 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-cruiserPackage.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:watchCI/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:dependenciesExample 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-importsFix: 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-domainFix: 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-servicesFix: 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:graphThis creates a dependency-graph.svg file showing:
- Module relationships
- Dependency directions
- Violations (highlighted in red)
Next Steps
- Understand the Modular Monolith Pattern
- Learn about Clean Architecture Layers
- Set up Integration Testing for module boundaries