Import Boundaries
Import boundary rules enforce layered architecture so routes do not reach into database internals and apps do not import sibling deployables.
Search across all documentation pages
Import boundary rules enforce layered architecture so routes do not reach into database internals and apps do not import sibling deployables.
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:
apps/ and packages/.routes → services → repositories.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-paths blocks specific folder-to-folder imports.no-extraneous-dependencies stops phantom deps from hoisting.target (importer glob) and from (forbidden source glob).apps/a importing apps/b.@nx/enforce-module-boundaries with tags for similar enforcement.{
zones: [
{
target: "./apps/**",
from: "./apps/**",
except: ["./apps/shared-config"],
message: "Apps import packages/, not other apps",
},
],
}.js extensions in import specifiers when "moduleResolution": "NodeNext".package.json exports - both should agree.../../packages/foo/src. Fix: import @acme/foo package name only.except globs for **/*.test.ts.index.ts becomes a loophole. Fix: restrict barrels or lint public API surface with knip.import-x/resolver-typescript with projectService.npm run lint required on PR.| 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 |
eslint-plugin-import-x is the maintained fork with flat config support. Prefer import-x for new projects.
Restrict cross-module imports via custom zones or Nest documented module boundaries; keep domain modules from importing infrastructure backwards.
Configure no-extraneous-dependencies with devDependencies file patterns including **/*.test.ts.
Separate ESLint config block for scripts/** with rules turned off or relaxed.
No. They catch structural mistakes; review still judges API design.
Static analysis may miss await import(variable). Boundaries cover static imports primarily.
packages/contracts should depend on nothing internal; all apps may import it.
Add a fixture file that intentionally violates a zone in test/fixtures excluded from prod or expect ESLint rule tests.
Autoload still resolves to files under src/routes; zones apply the same.
Combine no-restricted-paths with package.json exports and knip unused exports.
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.
Reviewed by Chris St. John·Last updated Jul 18, 2026