Skip to main content
Version: 7.0.0

Overview

JS Boundaries is a project that provides a set of tools to help you enforce architectural boundaries in your JavaScript and TypeScript projects.


Robert C. Martin's quote

"Software architecture is the art of drawing lines that I call boundaries. Those boundaries separate software elements from one another, and restrict those on one side from knowing about those on the other."

Clean Architecture: A Craftsman's Guide to Software Structure and Design


Purpose

It ensures that your architectural boundaries are respected by the elements in your project by checking the folder and file structure and the dependencies between them. At the moment, it consists of an ESLint plugin: eslint-plugin-boundaries.

How It Works

By default, it analyzes import and export statements, require calls, and dynamic import() expressions. You can customize it to inspect any other AST node that creates a dependency, such as jest.mock(). See the configuration guide for more details.

For each dependency it finds, the plugin classifies both the file the dependency comes from and the module it points to. That classification is what your rule policies read to decide whether the dependency is allowed.

The Concept

The plugin classifies every file along three independent layers. You configure the first two layers to recognize files and the elements they belong to; the third is derived for you.

LayerDescribesYou configure?Example
elementThe architectural role a file plays. Usually based on the folder it is in.Yes, with element descriptors.{ type: "controller", pattern: "controllers/*" }
fileA cross-cutting category of the file itself, independent of its element.Yes, with file descriptors.{ pattern: "**/*.spec.js", category: "test" }
moduleWhere the imported module resolves from.No — derived from the import.import "react" resolves to module.origin: "external"
  • An element is a group of files the plugin treats as one architectural unit — always a folder, like controllers/*. Element patterns match folder paths; they should not include file extensions. You map paths to a type, and every file under that folder belongs to that element.
  • A file category is a label attached to the file on its own, such as "test" or "style". File patterns can match individual files (e.g. **/*.spec.js). A file is independent of the element: the same file can be a controller and a test.
  • A module is the resolved target of a dependency. The plugin derives its origin"local" (your own files), "external" (a package), or "core" (a Node.js built-in) — so you can target third-party imports without naming each one.

The plugin builds a description for each dependency, which includes the three layers for both the from and to sides of the dependency, and it also carries a fourth, fully computed layer: the dependency metadata. It describes the nature of the import itself — its kind (a value, type, or typeof import), the structural relationship between the two elements, and the imported specifiers.

Dependency PropertyDescription
fromThe element, file, and module the dependency comes from.
toThe element, file, and module the dependency points to.
dependencyThe metadata about the dependency itself: its kind, relationship, specifiers, and so on.
Summary

Then, using selectors to match dependency descriptions, you define policies that allow or disallow them. Each policy can combine any of the three layers from both sides of the dependency, and the dependency metadata to express a boundary.

Because the layers are independent, a policy can mix them:

  • Models cannot import viewsfrom: { element: { type: "model" } }, disallow: { to: { element: { type: "view" } } }.
  • No code may import test filesdisallow: { to: { file: { categories: "test" } } }.
  • Only shared code may use the axios packagefrom: { element: { type: "!shared" } }, disallow: { to: { module: { source: "axios" } } }.
tip

Layering is progressive. Start with one classification layer — elements or files — and one policy, then add the remaining layers when you need them. You only need to configure one of the two; the rest is optional. See Classification.

Usage

1. Define the Classification in Your Project through Configuration

Map paths to element types, tag files with categories, or both — you only need one of the two layers:

const elementDescriptors = [
{ type: "controller", pattern: "controllers/*" },
{ type: "model", pattern: "models/*" },
{ type: "view", pattern: "views/*" },
{ type: "shared", pattern: "shared/*" },
];

const fileDescriptors = [
{ category: "test", pattern: "**/*.spec.js" },
];

2. The Plugin Builds a Runtime Description for Each Dependency

Given this configuration, the plugin analyzes your project at runtime and describes each dependency. For both sides of a dependency (from and to), it builds the three layers: the element the file belongs to, the file itself, and the resolved module. It also adds the dependency metadata. For example:

// Runtime description for a dependency in src/controllers/controller-a/index.js
{
from: {
element: {
types: ["controller"],
captured: { elementName: "controller-a" },
},
file: { categories: null },
module: { origin: "local" },
},
to: {
element: {
types: ["view"],
captured: { elementName: "view-a" },
},
file: { categories: null },
module: { origin: "local" },
},
dependency: {
kind: "value",
source: "@views/view-a",
specifiers: ["ViewA"],
},
}
note

This is a simplified view. See Classification for the full list of properties available in each description.

3. Define your Rule Policies Based on These Descriptions

Based on these descriptions, you can define rule policies to allow or disallow dependencies using selectors. For example:

Architecture Boundaries Diagram

const dependencyRulePolicies = [
// Allow controllers to depend on models and views
{
from: { element: { types: "controller" } },
allow: {
to: { element: { types: { anyOf: ["model", "view"] } },
},
},
// Allow views to depend on models
{
from: { element: { types: "view" } },
allow: {
to: { element: { types: "model" } },
},
},
// Disallow models to depend on anything other than other models
{
from: { element: { types: "model" } },
disallow: {
to: { element: { types: "!model" } },
},
},
// Disallow any element from importing a test file (a file layer match)
{
disallow: {
to: { file: { categories: "test" } },
},
},
];
note

This is a very simplified view. See Selectors for the full syntax and capabilities of selectors, and Policies for the full list of policy properties.

4. Get Instant Feedback

When a file violates a dependencies rule policy, ESLint reports an error. For example, a model importing a view:

// src/models/model-a/index.js
import View from "../../views/view-a";

ESLint reports:

error Dependencies to elements of type "view" are not allowed in elements of type "model". Denied by policy at index 2 boundaries/dependencies

Scope

This plugin focuses on enforcing architectural boundaries by analyzing the relationships between abstract elements. It does not inspect import syntax or enforce coding standards unrelated to module dependencies.

note

This plugin is not a replacement for eslint-plugin-import. In fact, using both together is recommended.

Quick Start

tip

Read the Quick Start Guide for step-by-step instructions on setting up the plugin in your project.