Dependency Cruiser: Restrict Imports in JavaScript

One pain point on ECMAScript/JavaScript projects is the lack of restrictions on where export-ed code can be import-ed. Any time you export a variable or function, it becomes visible to the entire rest of the project. On one hand, this is convenient: you don’t have to deal with moving code around when using it in new places, nor do you have to worry about layers of dependencies. Plus, your editor’s auto-import feature makes it easy to reuse code you know you’ve already written.

From a code-organization perspective, however, this can lead to a huge headache. Code from the client might start relying on server-side modules, or utilities might pull in business-specific logic. As the project grows, refactoring becomes a nightmare, bugs proliferate, and nobody’s quite sure which code depends on what. Code can unintentionally bypass architectural layers, making it difficult to separate concerns. Enter Dependency Cruiser.

Dependency Cruiser is an npm package that lets you define rules about how different parts of your application should interact. Instead of relying on developer discipline alone, it automates the enforcement of architectural boundaries — restrict imports before bad ones become entrenched in your codebase.

How does Dependency Cruiser work?

At its core, Dependency Cruiser lets you define rules that dictate which parts of your code can depend on others. For instance, in a microservices architecture, you might prevent imports between services, ensuring each remains isolated. In a layered application, you could enforce that your UI layer can only depend on the service layer, not on lower-level database or infrastructure code.

Once you set up your rules, Dependency Cruiser scans your project and reports any violations.

Here’s a basic example:


{
  "forbidden": [
    {
      "name": "no-client-server-mixing",
      "comment": "don't allow client-side code to import from server-side modules",
      "severity": "error",
      "from": { "path": "^src/client" },
      "to": { "path": "^src/server" }
    }
  ]
}

This rule ensures that no file inside the src/client directory can import from the src/server directory, helping you avoid accidental cross-environment dependencies.

Two approaches to dependency restrictions

In a Dependency Cruiser config, you have the option to “forbid” or “allow” dependencies. Each approach offers a distinct way of controlling how different parts of your codebase interact, depending on the level of flexibility or strictness you need.

The “forbid” approach works well in projects where flexibility is essential but there are a few dangerous dependency combinations you want to avoid. Think of it as setting up guardrails to avoid known risks. On the other hand, the “allow” approach is much stricter. It’s also better suited for projects with a clear architectural vision, where you want to be precise about which layers interact. In this scenario, only pre-approved dependencies can be introduced, creating a highly disciplined codebase structure.

Alternatives to Dependency Cruiser

A more lightweight alternative to Dependency Cruiser is ESLint‘s no-restricted-imports rule, which can restrict imports based on file paths or patterns. This rule works well for small projects where you need to prevent a few specific imports. However, it only supports the “forbid” approach, meaning you can block certain imports but can’t define an explicit set of allowed imports, as Dependency Cruiser does.

Benefits of structured dependencies

By setting up Dependency Cruiser, you gain confidence that your codebase is structured according to your design intentions. It encourages you to think about your project’s architecture and enforces clear boundaries between different layers. New developers don’t have to search through docs to figure out which dependencies are allowed — they can just run Dependency Cruiser and find out if they’ve broken any rules.

Take a moment to review your current project — are there imports happening where they shouldn’t? Dependency Cruiser can help bring order to your code and give your architecture the structure it needs to scale smoothly.

Conversation

Join the conversation

Your email address will not be published. Required fields are marked *