Custom Presets
Create internal presets for your organization. Create presets →
Learn how to configure Stricture in monorepo environments - from per-package configurations to shared presets and workspace-wide enforcement.
Stricture works seamlessly in monorepos with:
.stricture/config.jsonA typical monorepo with Stricture:
my-monorepo/├── packages/│ ├── api/│ │ ├── .stricture/│ │ │ └── config.json # API-specific config│ │ ├── src/│ │ ├── package.json│ │ └── tsconfig.json│ ├── web/│ │ ├── .stricture/│ │ │ └── config.json # Web-specific config│ │ ├── src/│ │ └── package.json│ ├── shared/│ │ ├── .stricture/│ │ │ └── config.json # Shared lib config│ │ └── src/│ └── stricture-config/ # Shared Stricture presets│ ├── src/│ │ ├── api-preset.ts│ │ ├── web-preset.ts│ │ └── index.ts│ └── package.json├── .eslintrc.js # Root ESLint config├── package.json├── pnpm-workspace.yaml└── turbo.jsonEach package can have its own architecture rules.
{ "preset": "@stricture/hexagonal"}packages/api/└── src/ ├── core/ │ ├── domain/ │ ├── ports/ │ └── application/ └── adapters/ ├── driving/ └── driven/{ "preset": "@stricture/nextjs"}packages/web/└── src/ ├── app/ ├── components/ ├── actions/ └── lib/{ "preset": "@stricture/layered", "boundaries": [ { "name": "types", "pattern": "src/types/**", "mode": "file", "tags": ["types"] }, { "name": "utils", "pattern": "src/utils/**", "mode": "file", "tags": ["utils"] }, { "name": "constants", "pattern": "src/constants/**", "mode": "file", "tags": ["constants"] } ], "rules": [ { "id": "utils-can-import-types", "from": { "tag": "utils" }, "to": { "tag": "types" }, "allowed": true }, { "id": "constants-isolated", "from": { "tag": "constants" }, "to": { "tag": "*" }, "allowed": false, "message": "Constants must be pure data with no dependencies" }, { "id": "constants-self", "from": { "tag": "constants" }, "to": { "tag": "constants" }, "allowed": true } ]}Create reusable configs for common patterns.
Create a shared config package:
{ "boundaries": [ { "name": "core", "pattern": "src/core/**", "mode": "file", "tags": ["core"] }, { "name": "infrastructure", "pattern": "src/infrastructure/**", "mode": "file", "tags": ["infrastructure"] } ], "rules": [ { "id": "core-isolation", "from": { "tag": "core" }, "to": { "tag": "infrastructure" }, "allowed": false } ]}Extend in packages:
{ "extends": "../stricture-config/base-config.json", "boundaries": [ { "name": "api", "pattern": "src/api/**", "mode": "file", "tags": ["api"] } ]}Create internal presets as workspace packages:
import type { ArchPreset } from '@stricture/core'
export const apiPreset: ArchPreset = { id: '@my-company/api-preset', name: 'Company API Architecture', description: 'Standard architecture for our API services', boundaries: [ { name: 'domain', pattern: 'src/domain/**', mode: 'file', tags: ['domain'], metadata: { layer: 0 } }, { name: 'application', pattern: 'src/application/**', mode: 'file', tags: ['application'], metadata: { layer: 1 } }, { name: 'infrastructure', pattern: 'src/infrastructure/**', mode: 'file', tags: ['infrastructure'], metadata: { layer: 2 } }, { name: 'api', pattern: 'src/api/**', mode: 'file', tags: ['api'], metadata: { layer: 3 } } ], rules: [ { id: 'domain-isolation', name: 'Domain Isolation', description: 'Domain has zero dependencies', severity: 'error', from: { tag: 'domain', mode: 'file' }, to: { tag: '*', mode: 'file' }, allowed: false, message: 'Domain must be pure business logic with no external dependencies' }, { id: 'domain-self', from: { tag: 'domain', mode: 'file' }, to: { tag: 'domain', mode: 'file' }, allowed: true }, { id: 'application-to-domain', from: { tag: 'application', mode: 'file' }, to: { tag: 'domain', mode: 'file' }, allowed: true }, { id: 'infrastructure-to-domain', from: { tag: 'infrastructure', mode: 'file' }, to: { tag: 'domain', mode: 'file' }, allowed: true }, { id: 'api-to-application', from: { tag: 'api', mode: 'file' }, to: { tag: 'application', mode: 'file' }, allowed: true } // Add more rules... ]}
export default apiPresetimport type { ArchPreset } from '@stricture/core'
export const webPreset: ArchPreset = { id: '@my-company/web-preset', name: 'Company Web Architecture', description: 'Standard architecture for our web apps', boundaries: [ { name: 'components', pattern: 'src/components/**', mode: 'file', tags: ['ui'] }, { name: 'services', pattern: 'src/services/**', mode: 'file', tags: ['services'] }, { name: 'api', pattern: 'src/api/**', mode: 'file', tags: ['api'] } ], rules: [ { id: 'ui-to-services', from: { tag: 'ui', mode: 'file' }, to: { tag: 'services', mode: 'file' }, allowed: true }, { id: 'services-to-api', from: { tag: 'services', mode: 'file' }, to: { tag: 'api', mode: 'file' }, allowed: true }, { id: 'no-ui-to-api', from: { tag: 'ui', mode: 'file' }, to: { tag: 'api', mode: 'file' }, allowed: false, message: 'UI components must use services, not call API directly' } ]}
export default webPresetexport { apiPreset } from './api-preset'export { webPreset } from './web-preset'{ "name": "@my-company/stricture-config", "version": "1.0.0", "type": "module", "exports": { "./api": { "types": "./dist/api-preset.d.ts", "import": "./dist/api-preset.js" }, "./web": { "types": "./dist/web-preset.d.ts", "import": "./dist/web-preset.js" } }, "scripts": { "build": "tsup" }, "dependencies": { "@stricture/core": "workspace:*" }, "devDependencies": { "tsup": "^8.0.0", "typescript": "^5.3.0" }}Use in packages:
{ "preset": "@my-company/stricture-config/api"}{ "preset": "@my-company/stricture-config/web"}packages: - 'packages/*'{ "name": "my-monorepo", "private": true, "scripts": { "lint": "pnpm -r run lint", "lint:stricture": "pnpm -r exec eslint . --rule '@stricture/enforce-boundaries: error'" }, "devDependencies": { "@stricture/eslint-plugin": "^0.1.0" }}Install dependencies:
# Install in all packagespnpm install -D -w @stricture/eslint-pluginRun linting:
# Lint all packagespnpm lint
# Lint specific packagepnpm --filter @my-company/api lint
# Lint with Stricture onlypnpm lint:stricture{ "$schema": "https://turbo.build/schema.json", "pipeline": { "build": { "dependsOn": ["^build"], "outputs": ["dist/**"] }, "lint": { "dependsOn": ["^build"], "outputs": [] }, "test": { "dependsOn": ["^build"], "outputs": [] } }}{ "scripts": { "lint": "turbo run lint", "lint:fix": "turbo run lint:fix" }}{ "scripts": { "lint": "eslint .", "lint:fix": "eslint . --fix" }}Run linting:
# Lint all packages (parallel)turbo lint
# Lint with cacheturbo lint --cache-dir=.turbo
# Force lint without cacheturbo lint --force{ "targetDefaults": { "lint": { "cache": true, "inputs": [ "default", "{workspaceRoot}/.eslintrc.json", "{workspaceRoot}/.stricture/**" ] } }}{ "name": "api", "targets": { "lint": { "executor": "@nx/linter:eslint", "options": { "lintFilePatterns": ["packages/api/**/*.ts"] } } }}Run linting:
# Lint specific projectnx lint api
# Lint all projectsnx run-many --target=lint --all
# Lint affected projects onlynx affected --target=lint{ "private": true, "workspaces": [ "packages/*" ], "scripts": { "lint": "yarn workspaces foreach run lint", "lint:api": "yarn workspace @my-company/api lint" }}Enforce boundaries across workspace packages.
You have a shared package and want to control which packages can import it:
packages/├── api/ # Can import shared├── web/ # Can import shared├── shared/ # Core utilities└── admin/ # Should NOT import shared{ "boundaries": [ { "name": "shared", "pattern": "src/**", "mode": "file", "tags": ["shared"] } ], "rules": [ { "id": "shared-self", "from": { "tag": "shared" }, "to": { "tag": "shared" }, "allowed": true }, { "id": "shared-no-external", "from": { "tag": "shared" }, "to": { "tag": "external" }, "allowed": false, "message": "Shared package must have minimal dependencies" } ]}Use ESLint import rules for cross-package restrictions:
module.exports = { overrides: [ { files: ['packages/admin/**/*.ts'], rules: { 'no-restricted-imports': [ 'error', { patterns: [ { group: ['@my-company/shared'], message: 'Admin package should not depend on shared utilities' } ] } ] } } ]}packages/├── api/ # Depends on: shared├── web/ # Depends on: shared, api (calls API)└── shared/ # Depends on: nothingStricture enforces file-level boundaries. For package-level circular deps, use your package manager:
# Prevent circular dependenciesstrict-peer-dependencies=truepnpm automatically detects circular package dependencies during install.
# Visualize package dependenciesnx graph
# Check for circular depsnx run-many --target=build --all# Fails if circular deps exist# Install madgenpm install -g madge
# Check for circular package depsmadge --circular --extensions ts,tsx packages/You can have a root .stricture/config.json that applies to all packages:
{ "boundaries": [ { "name": "packages", "pattern": "packages/*/src/**", "mode": "file", "tags": ["packages"] }, { "name": "tooling", "pattern": "tooling/**", "mode": "file", "tags": ["tooling"] } ], "rules": [ { "id": "tooling-independent", "from": { "tag": "tooling" }, "to": { "tag": "packages" }, "allowed": false, "message": "Tooling scripts should not import package code" } ]}However, per-package configs are recommended for clearer boundaries.
Let’s set up a complete monorepo with:
mkdir my-monorepo && cd my-monorepopnpm initecho "packages:\n - 'packages/*'" > pnpm-workspace.yamlmkdir my-monorepo && cd my-monoreponpm init -y# Add to package.json: "workspaces": ["packages/*"]mkdir my-monorepo && cd my-monorepoyarn init -y# Add to package.json: "workspaces": ["packages/*"]mkdir -p packages/{api,web,shared,stricture-config}/srcpnpm add -D -w @stricture/eslint-pluginimport type { ArchPreset } from '@stricture/core'import { hexagonalPreset, nextjsPreset } from '@stricture/eslint-plugin'
// Customize hexagonal for our APIsexport const apiPreset: ArchPreset = { ...hexagonalPreset, id: '@my-company/api-preset', name: 'Company API Preset', rules: [ ...hexagonalPreset.rules, { id: 'no-console', name: 'No Console in Domain', description: 'Domain should not use console.log', severity: 'error', from: { tag: 'domain', mode: 'file' }, to: { pattern: 'node_modules/console/**', mode: 'file' }, allowed: false, message: 'Use a logger service instead of console' } ]}
// Customize Next.js for our web appsexport const webPreset: ArchPreset = { ...nextjsPreset, id: '@my-company/web-preset', name: 'Company Web Preset'}{ "name": "@my-company/stricture-config", "version": "1.0.0", "type": "module", "main": "./dist/index.js", "types": "./dist/index.d.ts", "scripts": { "build": "tsup src/index.ts --format esm,cjs --dts" }, "dependencies": { "@stricture/core": "workspace:*", "@stricture/eslint-plugin": "workspace:*" }}Build the presets:
cd packages/stricture-configpnpm build{ "preset": "@my-company/stricture-config/api"}{ "name": "@my-company/api", "scripts": { "lint": "eslint ." }, "devDependencies": { "@my-company/stricture-config": "workspace:*" }}{ "preset": "@my-company/stricture-config/web"}{ "preset": "@stricture/layered", "boundaries": [ { "name": "utils", "pattern": "src/utils/**", "mode": "file", "tags": ["utils"] }, { "name": "types", "pattern": "src/types/**", "mode": "file", "tags": ["types"] } ], "rules": [ { "id": "types-isolated", "from": { "tag": "types" }, "to": { "tag": "*" }, "allowed": false, "message": "Types must be pure with no dependencies" }, { "id": "types-self", "from": { "tag": "types" }, "to": { "tag": "types" }, "allowed": true }, { "id": "utils-to-types", "from": { "tag": "utils" }, "to": { "tag": "types" }, "allowed": true } ]}module.exports = { root: true, plugins: ['@stricture'], rules: { '@stricture/enforce-boundaries': 'error' }, overrides: [ { files: ['packages/api/**/*.ts'], parserOptions: { project: './packages/api/tsconfig.json' } }, { files: ['packages/web/**/*.{ts,tsx}'], parserOptions: { project: './packages/web/tsconfig.json' } } ]}{ "scripts": { "lint": "pnpm -r run lint", "lint:fix": "pnpm -r run lint:fix", "build": "pnpm -r run build", "test": "pnpm -r run test" }}# Build all packagespnpm build
# Lint all packagespnpm lint
# Test API packagecd packages/apipnpm lintname: CI
on: [push, pull_request]
jobs: lint: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4
- uses: pnpm/action-setup@v2 with: version: 8
- uses: actions/setup-node@v4 with: node-version: '20' cache: 'pnpm'
- run: pnpm install --frozen-lockfile
- run: pnpm build
- run: pnpm lint name: Lint (includes Stricture)
- run: pnpm testname: CI
on: [push, pull_request]
jobs: lint: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4
- uses: pnpm/action-setup@v2
- uses: actions/setup-node@v4 with: node-version: '20' cache: 'pnpm'
- run: pnpm install
- name: Lint with Turbo run: pnpm turbo lintProblem: ESLint can’t find .stricture/config.json
Solution: Ensure config is in package root, not monorepo root
# ✅ Correctpackages/api/.stricture/config.json
# ❌ Wrong.stricture/config.jsonProblem: Workspace preset not found
Solution: Build preset package first
cd packages/stricture-configpnpm buildcd ../apipnpm lint # Now worksProblem: Stricture doesn’t prevent cross-package imports
Solution: Use ESLint no-restricted-imports rule
rules: { 'no-restricted-imports': [ 'error', { patterns: ['@my-company/admin'] } ]}Custom Presets
Create internal presets for your organization. Create presets →
Migration Guide
Migrate existing monorepo to Stricture. Migrate →
Troubleshooting
Debug common monorepo issues. Troubleshoot →