layerlock
v0.1.0
Published
Layer Lock: build-time architecture validation for TypeScript — layer boundaries, compiler-aware resolution, CI-friendly CLI.
Maintainers
Readme
Layer Lock (layerlock)
Build-time architecture validation for TypeScript. Declare layer folders and forbidden imports in layerlock.config.ts (legacy arch.config.ts is still discovered); CI fails when the static import graph breaks your rules. No runtime dependency in your application.
Keep NestJS, Express, Nx, or plain workspaces — layerlock only enforces which layers may import which.
In ~30 seconds
npm i -D layerlock typescriptlayerlock.config.ts (next to tsconfig.json; npx layerlock init creates this file):
import { defineArchitecture, layer } from "layerlock";
export default defineArchitecture({
tsconfig: "tsconfig.json",
layers: {
domain: ["src/domain/**/*.ts"],
infra: ["src/infra/**/*.ts"],
},
rules: [layer("domain").cannotImport("infra")],
});package.json: use "layerlock": "layerlock" so npm run layerlock and npm run layerlock check both validate (npm appends extra args to the script; avoid baking check into the script itself).
Run npm run check:arch, npm run layerlock, or npx layerlock. If someone adds import ... from domain into infra, you get a clear violation block (file, line, rule, hint) and exit code 1.
Use -f json for CI parsers; layerlock explain src/a.ts src/b.ts to see whether an edge is allowed.
Visual model (intent)
Onion-style layers: inner modules must not depend on outer modules. Allowed dependencies point inward; layerlock flags edges that go the wrong way.
flowchart TB
subgraph allowed["Allowed direction (simplified)"]
API["api / presentation"] --> APP["application / use-cases"]
APP --> DOM["domain"]
end
subgraph forbidden["Forbidden example"]
DOM2["domain"] -.->|cannot import| INF["infrastructure"]
end(Diagram is illustrative; your real globs and rules live in layerlock.config.ts.)
Contents
- In ~30 seconds
- Visual model
- What it solves
- How it works
- Requirements and footprint
- Install and quick start
- Real-world examples
- Ways to run
- Scaffold a layout
- Presets and rules
- Further documentation
- Why not ESLint?
- CLI reference
- AI-assisted development
- License and author
What it solves
Teams intend layered or modular dependencies, but the real import graph drifts: refactors, path aliases, and tooling-friendly edits still type-check while breaking that intent.
layerlock encodes allowed directions in layerlock.config.ts (or legacy arch.config.ts) and fails CI when a forbidden import appears. For why drift is costly, non-goals, and MVP analysis limits, see Overview and rationale.
How it works
- Layers — Name layers and map them with picomatch globs (for example
src/domain/**/*.ts). - Rules — Express forbidden edges with
layer("a").cannotImport("b"), optionalexceptFromfor tests or generated code. - TypeScript program — Load your
tsconfig.jsonand resolve imports the way the compiler does (paths,extends, typical monorepo setups). - Static analysis — Resolve statically visible import edges, classify them against layers, and report violations with location and hint (exact import forms covered: MVP limitations).
Validation runs at build or CI time. It does not replace Nest, Express, or Nx; it enforces import-level architecture on top of whatever stack you use.
Requirements and footprint
In your app: no runtime dependency — use layerlock as a devDependency only. Distribution: ESM, build target Node 18.
Environment: Node 18+, a normal TypeScript project with tsconfig.json, consume this package from ESM (or dynamic import()). Bundled npm dependencies and version notes: Requirements and dependencies.
Install and quick start
The fastest path is In ~30 seconds above. Here is the same flow with a three-layer example and a reminder about root:
npm i -D layerlock typescriptAdd layerlock.config.ts next to the tsconfig.json you want to enforce (or keep using legacy arch.config.ts). The CLI pins root to the config directory by default — avoid setting root yourself unless you have a specific reason.
import { defineArchitecture, layer } from "layerlock";
export default defineArchitecture({
tsconfig: "tsconfig.json",
layers: {
domain: ["src/domain/**/*.ts"],
infra: ["src/infra/**/*.ts"],
api: ["src/api/**/*.ts"],
},
rules: [
layer("domain").cannotImport("infra", "api"),
layer("api").cannotImport("infra"),
],
});package.json:
{
"scripts": {
"layerlock": "layerlock",
"check:arch": "layerlock"
}
}Run npm run check:arch, npm run layerlock, or npx layerlock. Exit code 1 on violations or configuration errors; 0 when clean.
Real-world examples
| Location | What to copy |
|----------|----------------|
| examples/simple | Minimal three-layer project with a deliberate violation (npm run layerlock). |
| examples/monorepo | Workspaces + tsconfig paths and cross-package boundaries. |
| docs/nestjs.md | Nest-style layering, presets.nestRecommended(), layerlock init --nest. |
See examples/README.md for commands.
Ways to run
CLI
The default command validates the project. Options are summarized in CLI reference; discovery rules are described in layerlock --help.
Programmatic
import { formatLayerlockText, layerlockCheck } from "layerlock";
const run = await layerlockCheck({ cwd: process.cwd() });
if (!run.ok) {
console.error(formatLayerlockText(run));
process.exit(1);
}Same configuration discovery as the CLI. Returns { ok, result, validated, configPath }.
Advanced embedding
For custom tooling, use resolveToValidatedArchitecture() and validate() (alias validateArchitecture) from the package exports (see src/public-api/ and src/config/).
Scaffold a layout
npx layerlock init --nest # four-layer preset + default test/e2e exceptions
npx layerlock init --clean # same folders; stricter defaults in tests
npx layerlock init --clean-arch # alias for --clean (empty repo → clean architecture layout)
npx layerlock init --hexagonal # ports/adapters: domain → ports → application → adapters
# npx layerlock init --clean-arch --force # replace existing layerlock.config.tsCreates layerlock.config.ts (via layerlock init), layer directories under src/ (with .gitkeep), and a minimal tsconfig.json only if none exists. Details of the layout are described in the NestJS and Express / minimal HTTP guides.
Presets and rules
| Preset | Use case |
|--------|----------|
| presets.layeredFromInnerToOuter | Generic onion: layers listed inner -> outer; inward imports forbidden. |
| presets.cleanArchitectureFourLayer | Opinionated four-layer names and default src/... globs. |
| presets.nestRecommended | Same onion as clean four-layer, plus default exceptFrom for tests and e2e (see NestJS guide). |
| presets.hexagonal | Ports and adapters naming (domain -> ports -> application -> adapters). |
| presets.dddLite | DDD-oriented layer names (domain, application, infrastructure, interfaces). |
| presets.nxStyle | Starting-point globs for libs/ / apps/ / packages/ style trees (tune per repo). |
Escape hatches: layer("a").cannotImport("b", { exceptFrom: ["**/*.spec.ts"] }) skips the rule for matching source files; ignoreFileGlobs on defineArchitecture excludes paths entirely (for example codegen).
import { defineArchitecture, presets } from "layerlock";
export default defineArchitecture({
tsconfig: "tsconfig.json",
...presets.nestRecommended({ baseDir: "src" }),
});Further documentation
The guide index is docs/README.md. Maintainer workflow: CONTRIBUTING.md.
Why not ESLint?
Many teams already use ESLint for style and correctness. layerlock does one thing: enforce named layer boundaries using the same import resolution as TypeScript, with a small API and presets.
A longer, honest comparison (including when ESLint or Nx is enough) is in docs/why-not-eslint.md.
CLI reference
| Item | Behavior |
|------|----------|
| layerlock or layerlock check | Validate (search upward for layerlock.config.*, then legacy arch.config.* / architecture.config.ts). |
| layerlock explain <from> <to> | Print each file’s layer and whether a cannotImport rule would forbid that edge (exit 1 if forbidden). |
| layerlock init --nest | --clean | --clean-arch | --hexagonal | Scaffold layerlock.config.ts and layer folders (--hexagonal: domain/ports/application/adapters + infrastructure); --clean-arch = --clean; --force overwrites the config file. |
| -c, --config <path> | Explicit configuration path (not combinable with --discover). |
| --discover | Find every layerlock.config.* / arch.config.* under the working directory and validate each package in parallel. |
| --watch | Re-run on file changes (daemon; does not exit). Not compatible with --graph. |
| --stable | Deterministic ordering for violations and JSON (diff-friendly CI logs). |
| --ci-diff | Stricter CI mode: implies --stable, normalizes paths to POSIX and repo-relative under each package root, POSIX --discover section headers, and deterministic JSON key order (stable field order per violation). |
| -f, --format text | json | Human-readable report (ASCII-friendly text: dash rules, -> layer edges, no box-drawing Unicode) or JSON (includes a summary object). |
| --graph mermaid | dot | none | Aggregate layer graph (single-package only). Without --graph-out, writes layerlock-layers.mmd or layerlock-layers.dot under the project root (directory of the config file unless root is set). --graph-out <path> overrides (path relative to cwd). Confirmation prints to stderr so stdout stays clean for -f json. |
| -h, --help | Help for validate, init, or explain. |
JSON schema: layerlock/schema (file schemas/layerlock.config.schema.json) for editor completion and agents.
Validate exit codes: 0 success, 1 violations or configuration error.
AI-assisted development
Fast refactors, path aliases, and AI-generated edits all increase the risk that the import graph drifts while tsc still passes. layerlock is designed to sit in CI and catch those regressions early.
- Edit
layerlock.config.tsusing the public API (defineArchitecture,layer,presets). - Run
layerlocklocally or in CI; failures include rule, location, resolved module, and actionable hints. - Use
-f jsonfor tools, dashboards, and agent loops that parse results. - Import
LAYERLOCK_AI_CONFIG_GUIDEfromlayerlockso assistants stay aligned with the configuration contract. - Use
layerlock explain <from.ts> <to.ts>when a failure (or a proposed edit) is unclear: it prints layers and whether the edge is allowed.
License and author
License: MIT — see LICENSE.
Author: Joao Otavio Carvalho Castejon.
Contributions: CONTRIBUTING.md.
