Import Boundaries
Import boundary rules enforce layered architecture so routes do not reach into database internals and apps do not import sibling deployables.
Recipe
Quick-reference recipe card - copy-paste ready.
npm install -D eslint-plugin-import-x{
rules: {
"import-x/no-restricted-paths": [
"error",
{
zones: [
{
target: "./src/routes",
from: "./src/db",
message: "Routes must call services, not db layer directly",
},
],
},
],
},
}When to reach for this:
- Monorepos with
apps/andpackages/. - Layered folders:
routes→services→repositories. - You need automated enforcement, not README diagrams alone.
Working Example
apps/orders-api/src/
routes/
services/
repositories/
db/
// eslint.config.js
import importX from "eslint-plugin-import-x";
export default [
{
plugins: { "import-x": importX },
files: ["apps/orders-api/src/**/*.ts"],
rules: {
"import-x/no-restricted-paths": [
"error",
{
zones: [
{
target: "./src/routes/**",
from: "./src/db/**",
},
{
target: "./src/routes/**",
from: "./src/repositories/**",
message: "Use services from routes",
},
{
target: "./src/services/**",
from: "./src/routes/**",
message: "Services must not import routes",
},
],
},
],
"import-x/no-extraneous-dependencies": [
"error",
{ devDependencies: ["**/*.test.ts", "eslint.config.js"] },
],
},
},
];// BAD: src/routes/orders.ts
import { pool } from "../db/pool.js"; // ESLint error
// GOOD: src/routes/orders.ts
import { createOrder } from "../services/orders.js";What this demonstrates:
no-restricted-pathsblocks specific folder-to-folder imports.no-extraneous-dependenciesstops phantom deps from hoisting.- Messages document the intended architecture in CI failures.
Deep Dive
How It Works
- ESLint resolves import paths relative to the file being linted.
- Zones define
target(importer glob) andfrom(forbidden source glob). - Monorepos add zones preventing
apps/aimportingapps/b. - Nx uses
@nx/enforce-module-boundarieswith tags for similar enforcement.
Monorepo Zone Example
{
zones: [
{
target: "./apps/**",
from: "./apps/**",
except: ["./apps/shared-config"],
message: "Apps import packages/, not other apps",
},
],
}TypeScript Notes
- Use
.jsextensions in import specifiers when"moduleResolution": "NodeNext". - Boundary rules complement
package.jsonexports- both should agree.
Gotchas
- Relative imports bypass package boundaries -
../../packages/foo/src. Fix: import@acme/foopackage name only. - Over-broad zones - Blocking legitimate shared test utilities. Fix:
exceptglobs for**/*.test.ts. - Barrel files re-export everything -
index.tsbecomes a loophole. Fix: restrict barrels or lint public API surface with knip. - False positives on path aliases - ESLint resolver not configured. Fix:
import-x/resolver-typescriptwithprojectService. - Rules without CI - Developers skip locally. Fix:
npm run lintrequired on PR.
Alternatives
| Alternative | Use When | Don't Use When |
|---|---|---|
| Nx module boundaries | Nx monorepo with tags | Single-package API |
| dependency-cruiser | Graph reports and CI gates | You only need ESLint in existing setup |
| Code review only | 2-person team prototype | Scale beyond one service |
FAQs
import-x vs import plugin?
eslint-plugin-import-x is the maintained fork with flat config support. Prefer import-x for new projects.
How do boundaries work with NestJS modules?
Restrict cross-module imports via custom zones or Nest documented module boundaries; keep domain modules from importing infrastructure backwards.
Can packages import devDependencies in tests?
Configure no-extraneous-dependencies with devDependencies file patterns including **/*.test.ts.
How do I allow scripts to import anything?
Separate ESLint config block for scripts/** with rules turned off or relaxed.
Do boundaries replace code review?
No. They catch structural mistakes; review still judges API design.
How are dynamic imports handled?
Static analysis may miss await import(variable). Boundaries cover static imports primarily.
What about shared types package?
packages/contracts should depend on nothing internal; all apps may import it.
How do I test boundary rules?
Add a fixture file that intentionally violates a zone in test/fixtures excluded from prod or expect ESLint rule tests.
Does Fastify autoload affect zones?
Autoload still resolves to files under src/routes; zones apply the same.
Can I enforce public API only?
Combine no-restricted-paths with package.json exports and knip unused exports.
Related
- Linting Basics - flat config setup
- Nx for Node Backends - tag-based boundaries
- Multi-Service Monorepo - app vs package rules
Stack versions: This page was written for Node.js 24.18.0 (Active LTS), npm 10+, TypeScript 5.6+, Express 5, Fastify 5, and NestJS 11.