Skip to main content
ESLint plugin · v7

Lint your architecture, not just your syntax.

Classify your files, write policies for who may depend on whom, and get an ESLint error the moment a boundary is crossed. In your IDE, in CI, with nothing new to run.

Read the quick start
npm i -D eslint-plugin-boundaries

Linting 4,000+ repositories · 7.4M downloads a month · ESLint 9 · Oxlint · TypeScript

3 element types · 1 rulemodules/*components/*/*helpers/*allow →allow →✕ helper → component

hover a layer · { type: "component", pattern: "components/*/*" }

Why

Architecture decays one import at a time.

The diagram on the wiki says helpers never know about components. Then someone needs a quick fix, the import is three keystrokes away, and the review is busy. JS Boundaries makes the diagram executable.

  1. The lines are in your configElement and file descriptors map folders and files to the names you already use in design discussions.
  2. The rules read both sidesEach policy sees the element, file category and module origin of the importer and the imported, plus the import kind.
  3. Feedback is immediateThe editor underlines the import. CI fails the build. The message explains the architectural reason, not just the rule id.
  4. Adoption is progressiveStart with one layer and one policy on an existing project with the recommended config. Tighten with strict when ready.
How it works

From folder tree to enforced architecture in three edits

Classify files and elements

Element descriptors match folders. File descriptors match files on their own, so a file can be a component and a test at the same time. You only need one of the two to start.

settings: {
  "boundaries/elements": [
    { type: "helper",    pattern: "helpers/*" },
    { type: "component", pattern: "components/*/*" },
    { type: "module",    pattern: "modules/*" },
  ],
  "boundaries/files": [
    { pattern: "**/*.spec.js", category: "test" },
  ],
}

Write the policies

Pick a default, then allow or disallow by any combination of element type, file category, module origin and dependency kind. Captured path fragments let one policy cover a whole family of elements.

"boundaries/dependencies": [2, {
  default: "disallow",
  policies: [
    { from: { element: { type: "component" } },
      allow: { to: { element: { type: "helper" } } } },
    { from: { element: { type: "module" } },
      allow: { to: { element: { types: ["component", "helper"] } } } },
    { disallow: { to: { file: { categories: "test" } } },
      message: "Never import test files from {{ from.element.types.[0] }}s" },
  ],
}]

Read the error where you wrote the import

Violations are ordinary ESLint errors: editor squiggles, CI failures, templated messages with the element types on both sides. Three more rules catch files outside your architecture, unknown dependencies and ignored ones.

src/helpers/format/index.js
  1:22  error  There is no policy allowing dependencies from
         elements of type "helper" to elements of type "component"
         boundaries/dependencies

✖ 1 problem (1 error, 0 warnings)
Specification

What the plugin understands

classification
Three independent layersElement (the architectural unit), file (its own category) and module (where the import resolves). All three are computed for both sides of every dependency.
selectors
Match on anything describedTypes, categories, origins, captured path fragments and array queries. Reference the from side's captures in the to side.
policies
Allow and disallow, with a defaultPolicies are evaluated in order. Messages are templates with access to the full description of the dependency.
rules
Four rules, one settingdependencies, no-unknown-files, no-unknown-dependencies, no-ignored-dependencies. Deprecated rules keep working.
performance
2.5× faster since 7.3Selectors are normalized once and message templates are cached, cutting lint time by 60% on real projects.
compatibility
ESLint 9, Oxlint, TypeScript, monoreposTyped config helpers (satisfies Config), custom resolvers and path aliases, per-package element roots.
New in v7

One file, described from every angle

Hover a layer to see the part of the runtime description it fills. Policies can read any of them, on both sides.

// components/Price/Price.spec.tsx  →  import "react"
{
  from: {
    element: { types: ["component"], captured: { name: "Price" } },
    file: { categories: ["test"] },
    module: { origin: "local" },
  },
  to: {
    element: { types: null },
    file: { categories: null },
    module: { origin: "external", source: "react" },
  },
  dependency: { kind: "value", relationship: { from: null, to: null } },
}
7.4Mmonthly downloads on npm
1,001GitHub stars, 4,000+ dependent repositories
6 yearsof releases since 2020, MIT licensed
“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.”
Robert C. Martin · Clean Architecture
Drafting table · coming next

Try a config against a sketched project

Paste settings, lay out a folder tree, and watch each file get classified and each import get its verdict. In the browser, nothing to install.

Follow the progress
src/
components/
Button/
Price/
helpers/
format/
modules/
checkout/
elementcomponent · captured { name: "Price" }
filecategories: [ "index" ]
moduleorigin: "local"
reporthelpers/format → components/Price · disallowed

Draw one boundary today

Install the plugin, name one folder, write one policy. Add the rest when the team is ready.