Hexagonal
Ports & Adapters with pure domain isolation. Learn more →
Understanding these core concepts will help you use Stricture effectively and create maintainable architecture rules.
Stricture enforces architecture by validating imports between boundaries using rules. When an import violates a rule, ESLint reports an error with a helpful message.
%%{init: {'theme':'base', 'themeVariables': { 'primaryColor':'#b3d9ff','primaryBorderColor':'#66b3ff','secondaryColor':'#b3ffcc','secondaryBorderColor':'#66cc99','tertiaryColor':'#ffffb3','tertiaryBorderColor':'#ffeb66'}}}%%
graph LR
I[Import Statement] --> B[Match Boundaries]
B --> R[Check Rules]
R --> D{Rule Matches?}
D -->|allowed: true| A[✅ Allow]
D -->|allowed: false| F[❌ Deny with message]
D -->|No match| DN[❌ Deny by default]
style A fill:#b3ffb3
style F fill:#ffb3b3
style DN fill:#ffb3b3
Stricture has a simple, focused package structure:
@stricture/eslint-plugin - Main package (install this)
@stricture/core@stricture/core - Validation engine
@stricture/cli - Command-line tools (optional)
All official architecture presets are bundled in @stricture/eslint-plugin:
stricture.configs.hexagonal() - Ports & Adaptersstricture.configs.layered() - N-tier architecturestricture.configs.clean() - Clean Architecturestricture.configs.modular() - Feature modulesstricture.configs.nextjs() - Next.js patternsstricture.configs.nestjs() - NestJS patternsEach preset provides:
Pattern 1: Inline Config (Recommended)
Use bundled presets directly in ESLint config:
import stricture from '@stricture/eslint-plugin'
export default [ stricture.configs.hexagonal() // Preset is bundled]npm install -D @stricture/eslint-pluginPattern 2: File-Based Config (Advanced)
Use .stricture/config.json for complex configurations:
{ "preset": "@stricture/hexagonal", "boundaries": [ /* custom boundaries */ ], "rules": [ /* many custom rules */ ]}import stricture from '@stricture/eslint-plugin'
export default [ stricture.configs.recommended() // Reads .stricture/config.json]Boundaries define the architectural layers in your codebase. Each boundary represents a group of files with a specific role.
A boundary is a named region of your codebase defined by a glob pattern:
{ "name": "domain", "pattern": "src/core/domain/**", "mode": "file", "tags": ["core", "domain"]}Properties:
name - Human-readable identifierpattern - Glob pattern matching files (e.g., src/domain/**)mode - Either "file" or "folder" matchingtags - Array of tags for rule matching{ "boundaries": [ { "name": "domain", "pattern": "src/core/domain/**", "mode": "file", "tags": ["domain"] }, { "name": "ports", "pattern": "src/core/ports/**", "mode": "file", "tags": ["ports"] }, { "name": "application", "pattern": "src/core/application/**", "mode": "file", "tags": ["application"] }, { "name": "adapters", "pattern": "src/adapters/**", "mode": "file", "tags": ["adapters"] } ]}File mode ("mode": "file"):
Folder mode ("mode": "folder"):
{ "boundaries": [ { "name": "user-module", "pattern": "src/modules/user/**", "mode": "folder", "tags": ["module"] } ]}Tags are labels that group boundaries for rules. A boundary can have multiple tags:
{ "name": "driving-adapters", "pattern": "src/adapters/driving/**", "tags": ["adapters", "driving"]}This allows rules like:
{ tag: "adapters" } - Matches all adapters{ tag: "driving" } - Matches only driving adaptersRules define which boundaries can import from which. A rule specifies:
{ "id": "domain-isolation", "name": "Domain Isolation", "description": "Domain layer must remain pure", "severity": "error", "from": { "tag": "domain" }, "to": { "tag": "*" }, "allowed": false, "message": "Domain cannot import from other layers or external libraries"}Properties:
id - Unique identifier (kebab-case)name - Human-readable namedescription - What the rule enforcesseverity - "error", "warn", or "off" (adds message prefix; use "off" to skip this rule)from - Source boundary pattern or tagto - Target boundary pattern or tagallowed - true allows import, false deniesmessage - Custom error message (optional)Match using glob patterns:
{ "id": "no-test-imports", "from": { "pattern": "src/**" }, "to": { "pattern": "**/*.test.ts" }, "allowed": false, "message": "Don't import test files in production code"}Match using boundary tags:
{ "id": "application-to-domain", "from": { "tag": "application" }, "to": { "tag": "domain" }, "allowed": true}{ "rules": [ { "id": "presentation-to-business", "name": "Presentation → Business", "severity": "error", "from": { "tag": "presentation" }, "to": { "tag": "business" }, "allowed": true }, { "id": "business-to-data", "name": "Business → Data", "severity": "error", "from": { "tag": "business" }, "to": { "tag": "data" }, "allowed": true }, { "id": "no-upward-dependencies", "name": "No Upward Dependencies", "severity": "error", "from": { "tag": "data" }, "to": { "tag": "business" }, "allowed": false, "message": "Data layer cannot depend on business layer - violates layered architecture" } ]}This enforces unidirectional flow:
%%{init: {'theme':'base', 'themeVariables': { 'primaryColor':'#b3d9ff','primaryBorderColor':'#66b3ff','secondaryColor':'#b3ffcc','secondaryBorderColor':'#66cc99'}}}%%
graph TB
P[Presentation Layer] --> B[Business Layer]
B --> D[Data Layer]
style P fill:#b3d9ff
style B fill:#b3ffcc
style D fill:#ffffb3
Presets are pre-configured sets of boundaries and rules for common architecture patterns.
A preset packages:
Instead of writing rules manually, use a preset:
{ "preset": "@stricture/hexagonal"}This single line gives you:
Hexagonal
Ports & Adapters with pure domain isolation. Learn more →
Layered
Traditional N-tier with unidirectional flow. Learn more →
Clean
Uncle Bob’s Clean Architecture with concentric layers. Learn more →
Next.js
Server/client boundaries for Next.js App Router. Learn more →
Override or add rules to presets:
{ "preset": "@stricture/hexagonal", "boundaries": [ { "name": "shared", "pattern": "src/shared/**", "tags": ["shared"] } ], "rules": [ { "id": "allow-shared", "from": { "pattern": "**" }, "to": { "tag": "shared" }, "allowed": true } ]}Your custom boundaries and rules are merged with the preset’s.
When multiple rules could match an import, the most specific rule wins - regardless of order in the config.
Stricture calculates a numeric score for each rule. Higher score = more specific.
Specificity hierarchy (highest to lowest):
Node modules pattern (10,000 points)
{ pattern: "node_modules/@types/**" }Regular pattern (1,000 points)
{ pattern: "src/domain/**" }Specific tag (100 points)
{ tag: "domain" }Wildcard tag (1 point)
{ tag: "*" }The total score is from_score + to_score.
{ "rules": [ { "id": "domain-isolation", "from": { "tag": "domain" }, // 100 points "to": { "tag": "*" }, // 1 point "allowed": false // Total: 101 }, { "id": "domain-self-imports", "from": { "tag": "domain" }, // 100 points "to": { "tag": "domain" }, // 100 points "allowed": true // Total: 200 ✅ MORE SPECIFIC } ]}For an import like:
import { User } from './user'; // domain → domainEvaluation:
domain-self-imports has higher specificity (200 > 101)Specificity lets you write general rules with specific exceptions:
{ "rules": [ // General rule: domain is isolated { "id": "domain-isolated", "from": { "tag": "domain" }, "to": { "tag": "*" }, "allowed": false }, // Specific exception: domain can import itself { "id": "domain-self", "from": { "tag": "domain" }, "to": { "tag": "domain" }, "allowed": true }, // Another exception: domain can import types { "id": "domain-types", "from": { "tag": "domain" }, "to": { "pattern": "node_modules/@types/**" }, "allowed": true } ]}The exceptions automatically override the general rule because they’re more specific.
Stricture uses a deny-by-default policy for safety.
For every import, Stricture:
allowed: true rule matches → Allowallowed: false rule matches → Deny with custom message%%{init: {'theme':'base', 'themeVariables': { 'primaryColor':'#b3d9ff','primaryBorderColor':'#66b3ff'}}}%%
graph TB
I[Import] --> M{Match Rule?}
M -->|allowed: true| A[✅ Allow]
M -->|allowed: false| D[❌ Deny
Custom Message]
M -->|No match| DD[❌ Deny
Generic Message]
style A fill:#b3ffb3
style D fill:#ffb3b3
style DD fill:#ffb3b3
Safety first: Better to error than allow unintended dependencies.
Example without explicit rule:
import _ from 'lodash'; // ❌ Denied by defaultError: Import from 'external' to 'application' is not allowed.
No rule explicitly allows this import.
To allow, add a rule:{ "from": { "tag": "application" }, "to": { "tag": "external" }, "allowed": true}Forces explicit decisions: Every cross-boundary import must be intentional.
allowed: falseOnly use allowed: false when:
Otherwise, rely on deny-by-default.
{ "rules": [ // ❌ Redundant - deny-by-default handles this { "id": "no-data-to-presentation", "from": { "tag": "data" }, "to": { "tag": "presentation" }, "allowed": false },
// ✅ Good - provides valuable guidance { "id": "domain-isolation", "from": { "tag": "domain" }, "to": { "tag": "*" }, "allowed": false, "message": "Domain must remain pure. Move infrastructure logic to adapters and use dependency injection." } ]}Stricture automatically detects imports from node_modules and treats them as a special external boundary.
external TagUse the special external tag to control npm package imports:
{ "id": "domain-no-externals", "from": { "tag": "domain" }, "to": { "tag": "external" }, "allowed": false, "message": "Domain cannot import external libraries - keep it pure"}Stricture detects external dependencies by checking if the import path contains node_modules:
// These are all externalimport _ from 'lodash'; // → node_modules/lodashimport { z } from 'zod'; // → node_modules/zodimport React from 'react'; // → node_modules/reactimport type { FastifyRequest } from 'fastify'; // → node_modules/@types/fastifyYou can allow specific npm packages using patterns:
{ "rules": [ { "id": "domain-no-deps", "from": { "tag": "domain" }, "to": { "tag": "external" }, "allowed": false }, { "id": "domain-can-use-types", "from": { "tag": "domain" }, "to": { "pattern": "node_modules/@types/**" }, "allowed": true, "message": "Domain can use TypeScript type definitions" } ]}The pattern rule is more specific than the tag rule, so it wins.
{ "rules": [ // Domain: no externals at all { "from": { "tag": "domain" }, "to": { "tag": "external" }, "allowed": false }, // Ports: only type definitions { "from": { "tag": "ports" }, "to": { "pattern": "node_modules/@types/**" }, "allowed": true }, // Application: utility libraries OK { "from": { "tag": "application" }, "to": { "pattern": "node_modules/{lodash,date-fns}/**" }, "allowed": true }, // Adapters: anything goes { "from": { "tag": "adapters" }, "to": { "tag": "external" }, "allowed": true } ]}Stricture uses glob patterns to match files and boundaries.
Common patterns:
| Pattern | Matches |
|---|---|
src/domain/** | All files in src/domain/ recursively |
src/domain/*.ts | Only top-level .ts files in src/domain/ |
**/*.test.ts | All test files anywhere |
src/{domain,ports}/** | Files in domain OR ports |
**/index.ts | All index.ts files |
{ "boundaries": [ { "name": "domain", "pattern": "src/core/domain/**", // All domain files "mode": "file" }, { "name": "test-files", "pattern": "**/*.{test,spec}.ts", // All test files "mode": "file" }, { "name": "utils", "pattern": "src/{shared,utils}/**", // Multiple folders "mode": "file" } ]}Use exclude to filter out specific files:
{ "name": "domain", "pattern": "src/domain/**", "exclude": ["**/*.test.ts", "**/mocks/**"]}Let’s see how all concepts work together in a complete example.
{ "boundaries": [ { "name": "domain", "pattern": "src/domain/**", "tags": ["domain"] }, { "name": "application", "pattern": "src/application/**", "tags": ["application"] }, { "name": "infrastructure", "pattern": "src/infrastructure/**", "tags": ["infrastructure"] } ], "rules": [ { "id": "domain-isolation", "from": { "tag": "domain" }, "to": { "tag": "*" }, "allowed": false, "message": "Domain must be pure - no external dependencies" }, { "id": "domain-self", "from": { "tag": "domain" }, "to": { "tag": "domain" }, "allowed": true }, { "id": "app-uses-domain", "from": { "tag": "application" }, "to": { "tag": "domain" }, "allowed": true }, { "id": "infra-implements", "from": { "tag": "infrastructure" }, "to": { "tag": "domain" }, "allowed": true } ]}%%{init: {'theme':'base', 'themeVariables': { 'primaryColor':'#b3d9ff','primaryBorderColor':'#66b3ff','secondaryColor':'#b3ffcc','secondaryBorderColor':'#66cc99','tertiaryColor':'#ffffb3','tertiaryBorderColor':'#ffeb66'}}}%%
graph TB
subgraph App["Application Layer"]
UC[Use Cases]
end
subgraph Dom["Domain Layer"]
E[Entities]
VO[Value Objects]
R[Repository Interfaces]
end
subgraph Infra["Infrastructure Layer"]
DB[Database]
API[External APIs]
end
UC -->|Uses| E
UC -->|Calls through| R
Infra -->|Implements| R
Infra -->|Uses| E
style Dom fill:#b3d9ff
style App fill:#b3ffcc
style Infra fill:#ffffb3
✅ Allowed:
❌ Forbidden:
// ✅ src/domain/user.ts - Pure domainexport class User { constructor( public readonly id: string, public readonly email: string ) {}}
// ✅ src/domain/user-repository.ts - Domain interfaceexport interface UserRepository { save(user: User): Promise<void>; findById(id: string): Promise<User | null>;}
// ✅ src/application/create-user.ts - Use caseimport { User } from '../domain/user';import { UserRepository } from '../domain/user-repository';
export class CreateUserUseCase { constructor(private repo: UserRepository) {}
async execute(email: string): Promise<User> { const user = new User(generateId(), email); await this.repo.save(user); return user; }}
// ✅ src/infrastructure/postgres-user-repository.ts - Implementationimport { User } from '../domain/user';import { UserRepository } from '../domain/user-repository';
export class PostgresUserRepository implements UserRepository { async save(user: User): Promise<void> { await this.db.query('INSERT INTO users...', [user.id, user.email]); }
async findById(id: string): Promise<User | null> { const row = await this.db.query('SELECT * FROM users WHERE id = $1', [id]); return row ? new User(row.id, row.email) : null; }}Now that you understand the core concepts:
Explore Presets
See how presets implement these concepts. Browse presets →
Custom Rules
Learn to write custom architecture rules. Write rules →
API Reference
Deep dive into configuration options. API docs →