01

A missing guardrail

ArchUnitTS began during a consulting project. The team needed the kind of executable architecture rules Java developers knew from ArchUnit, but the available TypeScript tools did not cover the project needs. The gap was practical: architectural boundaries existed in diagrams and conversations, but pull requests could still violate them without a deterministic signal.

Lukas Niessen started building the missing tool in his spare time. The first goal was deliberately small: describe a dependency rule in readable TypeScript and run it inside the test suite the project already trusted.

That delivery context shaped the library. The rule could not require a separate governance server, a new review ceremony, or a specialist to interpret the result. It had to live beside ordinary tests, follow the project configuration already in source control, and identify the relationship that made the decision fail.

architecture.test.ts
import { projectFiles } from 'archunit';

it('keeps presentation away from persistence', async () => {
  const rule = projectFiles()
    .inFolder('src/presentation/**')
    .shouldNot()
    .dependOnFiles()
    .inFolder('src/persistence/**');

  await expect(rule).toPassAsync();
});
02

Why the boundary belongs in a test

Architecture documentation is useful for explaining a system, but it is passive. It cannot notice that a controller imported a repository, that two feature packages formed a cycle, or that a supposedly isolated domain started depending on a framework. Code review can notice those changes, but only when the reviewer knows the decision, sees the relevant edge, and has enough time to follow the dependency path.

An architecture test turns that memory problem into repeatable evaluation. The test selects a part of the source tree, asks a structural question, and fails with evidence when the graph disagrees. The decision stays reviewable because it is expressed as code, and it stays current because the same check runs after every change.

This does not replace architecture diagrams or senior review. It gives both of them a feedback loop. Diagrams communicate the intended shape, review handles context and exceptions, and the executable rule protects the narrow invariant the team has agreed should always hold.

  • Run the rule with Jest, Vitest, Jasmine, or a generic check call.
  • Keep the architectural rationale beside the selector and condition.
  • Return concrete source paths instead of a generic policy failure.
  • Review changes to the rule in the same pull request as the design decision.
03

From a rule to a feedback system

Dependency direction was only the starting point. Real systems also need cycle detection, layer and slice policies, code metrics, diagrams, reports, and support for modern TypeScript project resolution. Each capability came from the same question: what evidence would help a team protect an architectural decision while the code is changing?

That framing keeps architecture testing close to delivery. Rules run locally, in pull requests, and in continuous integration. A violation points to source files and dependency paths, giving both people and coding agents a concrete next action.

The internal dependency graph became the reusable center of the product. A file rule can inspect direct edges, a layer rule can group endpoints by responsibility, a slice rule can derive repeated modules, and a report can render the same relationships for a different audience. The graph is not an incidental implementation detail. It is the shared evidence from which each architectural view is projected.

  • Fail when a selector matches nothing, so a renamed folder cannot create a false green result.
  • Use current TypeScript configuration, path aliases, and project references.
  • Export the same dependency graph as test output, JSON, Mermaid, D2, DOT, CSV, or HTML.
04

Follow the TypeScript project, not a shadow copy

Modern TypeScript systems are rarely a flat directory of relative imports. They use inherited tsconfig files, path aliases, package exports, project references, generated declarations, and different source roots across applications and libraries. An architecture test that resolves a different project from the compiler can be internally consistent and still answer the wrong question.

ArchUnitTS therefore uses TypeScript project configuration and compiler-backed module resolution. The analyzer follows include and exclude rules, compiler options, aliases, and referenced projects so that an import such as @payments/domain is connected to the source file the application actually builds.

This becomes especially important in monorepos. A duplicated architecture-only configuration drifts as workspaces evolve. Reading the real project removes that synchronization point and makes a failed rule easier to reproduce locally because the architecture test and the compiler begin from the same model.

05

A green test must prove that it checked something

Structural rules have a failure mode that ordinary assertions rarely face: the selector itself can become stale. If src/payment is renamed to src/payments, a naive rule may match no subjects, find no violations, and turn green. The architecture did not improve. The test stopped observing it.

ArchUnitTS treats an empty subject selection as a violation by default. A team can explicitly allow an empty result for optional modules, but the safe behavior is to report the unmatched filter. This protects the rule against directory moves, glob mistakes, and generated-code changes that would otherwise create false confidence.

The same principle applies to diagnostics more broadly. A useful guardrail makes its scope visible, preserves structured violations, and separates graph evaluation from the final test-runner assertion. That lets CI, reports, custom tooling, and coding agents consume the same result without scraping console text.

safe-selection.txt
selector matches files  -> evaluate condition -> pass or violations
selector matches nothing -> EmptyTestViolation
intentional empty scope   -> allowEmptyTests: true
06

One model, several delivery surfaces

The test result is only one representation of the architecture model. Teams also need to inspect a graph during a migration, attach an HTML or Mermaid artifact to a pull request, compare metrics over time, or give an automated tool machine-readable findings. Keeping violations and graph records structured makes those workflows possible without re-analyzing the repository for every consumer.

This matters in enterprise delivery because the people who define a boundary are not always the people who encounter it. A concise test failure helps the author of a change. A report helps a reviewer understand the surrounding shape. A stored JSON artifact helps platform automation aggregate recurring violations without inventing a second parser.

The objective is not a central dashboard for its own sake. It is one trustworthy extraction step with several views, each close to the decision being made.

07

An open-source family

Contributors joined, applications brought new edge cases, and the TypeScript project became the reference point for implementations in other ecosystems. The libraries do not force identical syntax. They share a recognizable mental model while respecting the language, package manager, and test runner of each community.

The origin still defines the direction: find a real architectural decision, express it as an executable rule, and keep the feedback close enough to the code that teams can act on it.

The family now provides a place to test that idea across very different language systems. Python uses its AST, .NET uses Roslyn, Ruby uses Prism, Rust follows Cargo and module semantics, Zig starts from explicit compilation units, and Go uses its own package tooling. Shared concepts can move between implementations, but source extraction and developer experience remain language-native responsibilities.

For a team adopting ArchUnit today, the recommended path is still the original one: choose a boundary whose violation creates real cost, express it in the smallest clear rule, make the current code pass, and add it to the existing delivery pipeline. A durable architecture suite grows from useful decisions, not from a template containing every possible policy.