Overview
JS Boundaries is a project that provides a set of tools to help you enforce architectural boundaries in your JavaScript and TypeScript projects.
"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. You can also use it with Oxlint.
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.
- You classify each file or folder in the project using descriptors in your configuration.
- The plugin classifies each dependency at runtime, building a description for both sides of the dependency.
- You define policies allowing or disallowing dependencies based on the descriptions. Each policy can combine any of the three layers from both sides of the dependency, and the dependency metadata to express a boundary.
Usage
1. Define the Classification in Your Project through Configuration
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.
| Layer | Describes | You configure? | Example |
|---|---|---|---|
| element | The architectural role a file plays. Usually based on the folder it is in. | Yes, with element descriptors. | { type: "controller", pattern: "controllers/*" } |
| file | A cross-cutting category of the file itself, independent of its element. | Yes, with file descriptors. | { pattern: "**/*.spec.js", category: "test" } |
| module | Where the imported module resolves from. | No — derived from the import. | import "react" resolves to module.origin: "external" |
Configuration is done in your eslint.config.js file, in the boundaries settings. For example:
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" },
];
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.
2. The Plugin Builds a Runtime Description for Each Dependency
Given this configuration, 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 Property | Description |
|---|---|
| from | The element, file, and module the dependency comes from. |
| to | The element, file, and module the dependency points to. |
| dependency | The metadata about the dependency itself: its kind, relationship, specifiers, and so on. |
Click to expand an example of a runtime description
// 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"],
},
}
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 define rule policies to allow or disallow dependencies using selectors.
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.
const dependencyRulePolicies = [
// Allow controllers to depend on models and views
{
from: { element: { type: "controller" } },
allow: {
to: { element: { types: { anyOf: ["model", "view"] } } },
},
},
// Allow views to depend on models
{
from: { element: { type: "view" } },
allow: {
to: { element: { type: "model" } },
},
},
// Disallow any element from importing a test file
{
disallow: {
to: { file: { categories: "test" } },
},
},
// Only controllers may use the `axios` package
{
from: { element: { type: "!controller" } },
disallow: {
to: { module: { origin: "external", source: "axios" } },
},
},
];
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.
This plugin is not a replacement for eslint-plugin-import. In fact, using both together is recommended.
Quick Start
Read the Quick Start Guide for step-by-step instructions on setting up the plugin in your project.