Skip to content

Monorepo Setup

Learn how to configure Stricture in monorepo environments - from per-package configurations to shared presets and workspace-wide enforcement.

Overview

Stricture works seamlessly in monorepos with:

  • Per-package configurations - Each package can have its own .stricture/config.json
  • Shared configurations - Reuse common rules across packages
  • Custom workspace presets - Internal presets for your organization
  • Workspace dependencies - Enforce boundaries across packages
  • Turborepo/pnpm/NX integration - Works with all major monorepo tools

Monorepo Structure

A 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.json

Per-Package Configurations

Each package can have its own architecture rules.

Example: API Package (Hexagonal)

packages/api/.stricture/config.json
{
"preset": "@stricture/hexagonal"
}
packages/api/
└── src/
├── core/
│ ├── domain/
│ ├── ports/
│ └── application/
└── adapters/
├── driving/
└── driven/

Example: Web Package (Next.js)

packages/web/.stricture/config.json
{
"preset": "@stricture/nextjs"
}
packages/web/
└── src/
├── app/
├── components/
├── actions/
└── lib/

Example: Shared Package (Layered)

packages/shared/.stricture/config.json
{
"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
}
]
}

Shared Configurations

Create reusable configs for common patterns.

Option 1: Shared JSON Config

Create a shared config package:

packages/stricture-config/base-config.json
{
"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:

packages/api/.stricture/config.json
{
"extends": "../stricture-config/base-config.json",
"boundaries": [
{
"name": "api",
"pattern": "src/api/**",
"mode": "file",
"tags": ["api"]
}
]
}

Create internal presets as workspace packages:

packages/stricture-config/src/api-preset.ts
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 apiPreset
packages/stricture-config/src/web-preset.ts
import 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 webPreset
packages/stricture-config/src/index.ts
export { apiPreset } from './api-preset'
export { webPreset } from './web-preset'
packages/stricture-config/package.json
{
"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:

packages/api/.stricture/config.json
{
"preset": "@my-company/stricture-config/api"
}
packages/web/.stricture/config.json
{
"preset": "@my-company/stricture-config/web"
}

Workspace Tools Integration

pnpm Workspaces

pnpm-workspace.yaml
packages:
- 'packages/*'
package.json
{
"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:

Terminal window
# Install in all packages
pnpm install -D -w @stricture/eslint-plugin

Run linting:

Terminal window
# Lint all packages
pnpm lint
# Lint specific package
pnpm --filter @my-company/api lint
# Lint with Stricture only
pnpm lint:stricture

Turborepo

turbo.json
{
"$schema": "https://turbo.build/schema.json",
"pipeline": {
"build": {
"dependsOn": ["^build"],
"outputs": ["dist/**"]
},
"lint": {
"dependsOn": ["^build"],
"outputs": []
},
"test": {
"dependsOn": ["^build"],
"outputs": []
}
}
}
package.json
{
"scripts": {
"lint": "turbo run lint",
"lint:fix": "turbo run lint:fix"
}
}
packages/api/package.json
{
"scripts": {
"lint": "eslint .",
"lint:fix": "eslint . --fix"
}
}

Run linting:

Terminal window
# Lint all packages (parallel)
turbo lint
# Lint with cache
turbo lint --cache-dir=.turbo
# Force lint without cache
turbo lint --force

NX Workspaces

nx.json
{
"targetDefaults": {
"lint": {
"cache": true,
"inputs": [
"default",
"{workspaceRoot}/.eslintrc.json",
"{workspaceRoot}/.stricture/**"
]
}
}
}
packages/api/project.json
{
"name": "api",
"targets": {
"lint": {
"executor": "@nx/linter:eslint",
"options": {
"lintFilePatterns": ["packages/api/**/*.ts"]
}
}
}
}

Run linting:

Terminal window
# Lint specific project
nx lint api
# Lint all projects
nx run-many --target=lint --all
# Lint affected projects only
nx affected --target=lint

Yarn Workspaces

package.json
{
"private": true,
"workspaces": [
"packages/*"
],
"scripts": {
"lint": "yarn workspaces foreach run lint",
"lint:api": "yarn workspace @my-company/api lint"
}
}

Cross-Package Dependencies

Enforce boundaries across workspace packages.

Scenario: Shared Package Access

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

Option 1: Package-Level Rules

packages/shared/.stricture/config.json
{
"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"
}
]
}

Option 2: ESLint Import Rules

Use ESLint import rules for cross-package restrictions:

.eslintrc.js
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'
}
]
}
]
}
}
]
}

Scenario: Preventing Circular Package Dependencies

packages/
├── api/ # Depends on: shared
├── web/ # Depends on: shared, api (calls API)
└── shared/ # Depends on: nothing

Stricture enforces file-level boundaries. For package-level circular deps, use your package manager:

.npmrc
# Prevent circular dependencies
strict-peer-dependencies=true

pnpm automatically detects circular package dependencies during install.

Root-Level Configuration

You can have a root .stricture/config.json that applies to all packages:

.stricture/config.json
{
"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.

Example: Real Monorepo Setup

Let’s set up a complete monorepo with:

  • API (Hexagonal)
  • Web (Next.js)
  • Shared (Utilities)
  • Internal presets

Step 1: Initialize Monorepo

Terminal window
mkdir my-monorepo && cd my-monorepo
pnpm init
echo "packages:\n - 'packages/*'" > pnpm-workspace.yaml

Step 2: Create Packages

Terminal window
mkdir -p packages/{api,web,shared,stricture-config}/src

Step 3: Install Stricture (Root)

Terminal window
pnpm add -D -w @stricture/eslint-plugin

Step 4: Create Internal Presets

packages/stricture-config/src/index.ts
import type { ArchPreset } from '@stricture/core'
import { hexagonalPreset, nextjsPreset } from '@stricture/eslint-plugin'
// Customize hexagonal for our APIs
export 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 apps
export const webPreset: ArchPreset = {
...nextjsPreset,
id: '@my-company/web-preset',
name: 'Company Web Preset'
}
packages/stricture-config/package.json
{
"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:

Terminal window
cd packages/stricture-config
pnpm build

Step 5: Configure Each Package

packages/api/.stricture/config.json
{
"preset": "@my-company/stricture-config/api"
}
packages/api/package.json
{
"name": "@my-company/api",
"scripts": {
"lint": "eslint ."
},
"devDependencies": {
"@my-company/stricture-config": "workspace:*"
}
}
packages/web/.stricture/config.json
{
"preset": "@my-company/stricture-config/web"
}
packages/shared/.stricture/config.json
{
"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
}
]
}

Step 6: Root ESLint Config

.eslintrc.js
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'
}
}
]
}

Step 7: Root Scripts

package.json
{
"scripts": {
"lint": "pnpm -r run lint",
"lint:fix": "pnpm -r run lint:fix",
"build": "pnpm -r run build",
"test": "pnpm -r run test"
}
}

Step 8: Verify Setup

Terminal window
# Build all packages
pnpm build
# Lint all packages
pnpm lint
# Test API package
cd packages/api
pnpm lint

CI Integration

GitHub Actions

.github/workflows/ci.yml
name: 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 test

Turborepo + GitHub Actions

.github/workflows/ci.yml
name: 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 lint

Best Practices

Do’s

  • Use workspace presets for shared rules
  • Keep configs simple per package
  • Version presets independently
  • Document architecture at monorepo level
  • Test presets in dedicated package
  • Cache builds in CI (Turbo/NX)

Don’ts

  • Don’t duplicate rules across packages (use presets)
  • Don’t mix architectures without reason
  • Don’t skip linting in some packages
  • Don’t ignore workspace boundaries (use ESLint import rules)

Troubleshooting

Config Not Found

Problem: ESLint can’t find .stricture/config.json

Solution: Ensure config is in package root, not monorepo root

Terminal window
# ✅ Correct
packages/api/.stricture/config.json
# ❌ Wrong
.stricture/config.json

Preset Not Resolving

Problem: Workspace preset not found

Solution: Build preset package first

Terminal window
cd packages/stricture-config
pnpm build
cd ../api
pnpm lint # Now works

Cross-Package Imports

Problem: Stricture doesn’t prevent cross-package imports

Solution: Use ESLint no-restricted-imports rule

rules: {
'no-restricted-imports': [
'error',
{ patterns: ['@my-company/admin'] }
]
}

Next Steps

Migration Guide

Migrate existing monorepo to Stricture. Migrate →