Architectural versioning*: Rule sets can evolve alongside the product, with historical violations tracked and remediated systematically.
The tooling ecosystem now supports this shift natively. dependency-cruiser provides the scanning engine, rule evaluation, and visualization pipeline required to operationalize architectural governance without custom tooling development.
Core Solution
Implementing automated dependency validation requires a structured approach: installation, baseline scanning, rule definition, visualization integration, and CI enforcement. Each step builds upon the previous one, ensuring the configuration remains maintainable and aligned with actual project topology.
Step 1: Installation and Baseline Scan
Begin by installing the package as a development dependency. The tool operates independently of bundlers or frameworks, making it universally applicable across Node.js, React, Vue, Angular, and TypeScript monorepos.
npm install --save-dev dependency-cruiser
Run an initial scan against your source directory to establish a baseline. This reveals the current state of your import graph without enforcing any constraints.
npx depcruise src --output-type json > .dep-cruise-baseline.json
The JSON output contains every resolved module, its dependencies, and dependency types. Review this file to identify existing circular references, heavily coupled directories, and unused modules before writing rules.
Step 2: Defining Structural Constraints
Rules are declared in a configuration file. The schema uses a forbidden array where each entry specifies a violation pattern. Below is a production-ready configuration for a TypeScript monorepo with strict layer boundaries. Note the use of workspace aliases, explicit path regexes, and severity levels.
// .dependency-cruiser.js
module.exports = {
forbidden: [
{
name: 'no-circular-imports',
severity: 'error',
from: {},
to: {
circular: true
}
},
{
name: 'ui-must-not-import-data-layer',
severity: 'error',
from: {
path: '^src/(ui|components|pages)'
},
to: {
path: '^src/(data|repositories|api-clients)'
}
},
{
name: 'core-must-not-import-features',
severity: 'warn',
from: {
path: '^src/core'
},
to: {
path: '^src/features'
}
},
{
name: 'no-external-deps-in-utils',
severity: 'error',
from: {
path: '^src/utils'
},
to: {
dependencyTypes: ['npm', 'yarn', 'pnpm']
}
}
],
options: {
tsConfig: {
fileName: 'tsconfig.json'
},
doNotFollow: {
path: 'node_modules'
},
detectiveOptions: {
ts: {
detectImports: true,
includeDynamic: false
}
}
}
};
Architecture Rationale:
no-circular-imports is set to error because circular references break module initialization order and cause unpredictable runtime behavior.
ui-must-not-import-data-layer enforces unidirectional data flow. UI components should receive data through props or state management, not direct API calls.
core-must-not-import-features uses warn severity during transition periods. Core utilities should remain framework-agnostic; feature-specific imports indicate misplaced abstraction.
no-external-deps-in-utils prevents utility directories from becoming dependency sinks. Shared helpers should only depend on other local modules or standard library functions.
Step 3: Visualization Pipeline
Static analysis reveals topology; visualization reveals patterns. Generate a DOT graph and convert it to SVG for architectural review sessions.
npx depcruise src --include-only "^src" --output-type dot | dot -T svg > docs/dependency-graph.svg
For large projects, filter the output to specific subsystems:
npx depcruise src/features --include-only "^src/features" --output-type dot | dot -T svg > docs/features-graph.svg
Visualization serves two purposes: onboarding new developers to the codebase structure, and identifying coupling hotspots before they violate explicit rules.
Step 4: CI/CD Integration
Validation must run in continuous integration to prevent architectural drift. Add a dedicated job that fails the pipeline when rules are violated.
# .github/workflows/architecture-validation.yml
name: Architecture Validation
on: [pull_request]
jobs:
dependency-cruise:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 20
- run: npm ci
- run: npx depcruise src --validate .dependency-cruiser.js --config .dependency-cruiser.js
Failing the build on structural violations ensures that architecture is treated with the same rigor as type safety or test coverage.
Pitfall Guide
1. Over-Constraining Early-Stage Projects
Explanation: Applying strict layer boundaries before the architecture stabilizes creates friction. Teams spend more time disabling rules than shipping features.
Fix: Start with circular: true detection only. Introduce layer rules incrementally as domain boundaries solidify. Use warn severity during transition phases.
2. Ignoring Bundler Path Aliases
Explanation: dependency-cruiser resolves imports using Node.js module resolution by default. Vite, Webpack, and TypeScript path aliases (@app/core) will fail to resolve without explicit configuration.
Fix: Always provide tsConfig.fileName in the options block. For Webpack-specific aliases, use the webpackConfig.fileName option or map aliases manually in the options.alias array.
3. Treating Shared Directories as Free-For-Alls
Explanation: /utils, /helpers, and /shared directories naturally attract cross-cutting imports. Without constraints, they become architectural dumping grounds that increase coupling.
Fix: Enforce a rule that shared modules cannot import from feature-specific directories. Consider splitting monolithic shared folders into domain-scoped utilities (/utils/date, /utils/formatting).
4. Running Validation Only Locally
Explanation: Local pre-commit hooks catch violations for the author but miss contributions from other team members or automated dependency updates.
Fix: Run validation in CI on every pull request. Use pre-commit hooks only for fast feedback; treat CI as the source of truth.
5. Misconfiguring Dynamic Import Handling
Explanation: Dynamic import() statements are often used for code splitting. If the tool attempts to statically analyze them as hard dependencies, it may report false circular references.
Fix: Set detectiveOptions.ts.includeDynamic: false unless you specifically need to track dynamic import boundaries. Document this decision in the config comments.
6. Neglecting Unused Module Detection
Explanation: Dead code accumulates silently. Unused modules increase bundle size and confuse developers about active architecture boundaries.
Fix: Enable options.reportUnused or run npx depcruise src --output-type err-long to surface orphaned files. Schedule quarterly cleanup sprints to remove them.
7. Hardcoding Absolute Paths Instead of Workspace Patterns
Explanation: Monorepos with multiple packages require relative path matching that adapts to package boundaries. Hardcoded ^src/ patterns break when scanning individual workspaces.
Fix: Use workspace-aware regex patterns or run the cruiser per-package with --include-only scoped to each workspace root. Leverage dependency-cruiser's monorepo plugin if applicable.
Production Bundle
Action Checklist
Decision Matrix
| Scenario | Recommended Approach | Why | Cost Impact |
|---|
| Startup MVP (< 3 months) | Circular detection only | Minimizes friction while preventing runtime initialization failures | Low setup cost, high stability gain |
| Mid-size team (5-15 devs) | Layer boundaries + circular rules | Enforces separation of concerns across parallel feature development | Moderate config overhead, reduced merge conflicts |
| Enterprise monorepo | Workspace-scoped rules + unused module detection | Prevents cross-package coupling and bundle bloat | Higher initial config, significant long-term maintenance savings |
| Legacy codebase migration | Warn severity + gradual enforcement | Allows incremental refactoring without blocking releases | Zero deployment delay, predictable debt reduction |
Configuration Template
// .dependency-cruiser.js
module.exports = {
forbidden: [
{
name: 'no-circular',
severity: 'error',
from: {},
to: { circular: true }
},
{
name: 'enforce-unidirectional-data-flow',
severity: 'error',
from: { path: '^src/(ui|views|components)' },
to: { path: '^src/(data|services|repositories)' }
},
{
name: 'protect-core-abstractions',
severity: 'warn',
from: { path: '^src/core' },
to: { path: '^src/(features|modules)' }
},
{
name: 'isolate-shared-utilities',
severity: 'error',
from: { path: '^src/shared' },
to: { dependencyTypes: ['npm', 'yarn', 'pnpm'] }
}
],
options: {
tsConfig: { fileName: 'tsconfig.json' },
doNotFollow: { path: 'node_modules' },
detectiveOptions: {
ts: { detectImports: true, includeDynamic: false }
},
reporterOptions: {
dot: {
theme: {
graph: { rankdir: 'TB' },
node: { fontsize: 11 }
}
}
}
}
};
Quick Start Guide
- Initialize: Run
npm install --save-dev dependency-cruiser in your project root.
- Baseline: Execute
npx depcruise src --output-type json > baseline.json to map current imports.
- Configure: Copy the configuration template above into
.dependency-cruiser.js and adjust path regexes to match your directory structure.
- Validate: Run
npx depcruise src --validate .dependency-cruiser.js locally to verify rule behavior.
- Automate: Add the validation command to your CI pipeline and commit the configuration file. Architecture governance is now active.