SkillAgentSearch skills...

ArchUnitTS

ArchUnitTS is an architecture testing library. Specify and ensure architecture rules in your TypeScript app. Easy setup and pipeline integration.

Install / Use

/learn @LukasNiessen/ArchUnitTS

README

ArchUnitTS - Architecture Testing

<div align="center" name="top"> <img align="center" src="assets/logo-rounded.png" width="150" height="150" alt="ArchUnitTS Logo"> <!-- spacer --> <p></p>

License: MIT npm version npm downloads GitHub stars

</div>

Enforce architecture rules in TypeScript and JavaScript projects. Check for dependency directions, detect circular dependencies, enforce coding standards and much more. Integrates with every testing framework. Very simple setup and pipeline integration.

The #1 architecture testing library for TypeScript, measured by GitHub stars.

Inspired by the amazing ArchUnit library but we are not affiliated with ArchUnit.

SetupDemoUse CasesFeaturesWhy ArchUnitTS?ContributingDocumentation

⚡ 5 min Quickstart

Installation

npm install archunit --save-dev

Add tests

Simply add tests to your existing test suites. The following is an example using Jest. First we ensure that we have no circular dependencies.

import { projectFiles, metrics } from 'archunit';

it('should not have circular dependencies', async () => {
  const rule = projectFiles().inFolder('src/**').should().haveNoCycles();
  await expect(rule).toPassAsync();
});

Next we ensure that our layered architecture is respected.

it('presentation layer should not depend on database layer', async () => {
  const rule = projectFiles()
    .inFolder('src/presentation/**')
    .shouldNot()
    .dependOnFiles()
    .inFolder('src/database/**');

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

it('business layer should not depend on database layer', async () => {
  const rule = projectFiles()
    .inFolder('src/business/**')
    .shouldNot()
    .dependOnFiles()
    .inFolder('src/database/**');

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

// More layers ...

Lastly we ensure that some code metric rules are met.

it('should not contain too large files', async () => {
  const rule = metrics().count().linesOfCode().shouldBeBelow(1000);
  await expect(rule).toPassAsync();
});

it('should only have classes with high cohesion', async () => {
  // LCOM metric (lack of cohesion of methods), low = high cohesion
  const rule = metrics().lcom().lcom96b().shouldBeBelow(0.3);
  await expect(rule).toPassAsync();
});

CI Integration

These tests will run automatically in your testing setup, for example in your CI pipeline, so that's basically it. This setup ensures that the architectural rules you have defined are always adhered to! 🌻🐣

Additionally, you can generate reports and save them as artifacts. Here's a simple example using GitLab CI. Note that reports are in beta.

it('should generate HTML reports', async () => {
  const countMetrics = metrics().count();
  const lcomMetrics = metrics().lcom();

  // Saves HTML report files to /reports
  await countMetrics.exportAsHTML();
  await lcomMetrics.exportAsHTML();

  // So we get no warning about an empty test
  expect(0).toBe(0);
});

In your gitlab-ci.yml:

test:
  script:
    - npm test
  artifacts:
    when: always
    paths:
      - reports

🚐 Setup

Installation:

npm install archunit --save-dev

If you're using Jest, that's it already. For Vitest, Jasmine or any other framework, please read below.

We have added special syntax for Jest, Vitest and Jasmine: toPassAsync(). We strongly recommend using it. Many benefits come with it, for example beautiful error messages in case of a failing tests.

Jest

Works out of the box.

Vitest

Works out of the box too, but you must have configured Vitest with globals: true in your vitest.config.ts. This means you need a vitest.config.ts file at project root with content that may look like this:

import { defineConfig } from 'vitest/config';

export default defineConfig({
	test: {
		globals: true,  // This line matters !!
		...
	},
});

Jasmine

Jasmine unfortunately has some constraints and requires minimal setup. However, you will need just one line of code:

beforeEach(() => {
  jasmine.addAsyncMatchers(jasmineMatcher);
});

Include this in your test files. And, since this is an asynchronous matcher, you must use expectAsync with Jasmine, not expect. Example:

describe('architecture', () => {
	beforeEach(() => {
		jasmine.addAsyncMatchers(jasmineMatcher);
	});

	it('business logic should not depend on the ui', async () => {
		const rule = projectFiles()
			.inFolder('business')
			.shouldNot()
			.dependOnFiles()
			.inFolder('ui');

		await expectAsync(rule).toPassAsync(); // expectAsync, not expect !!
	});

Other Framework

If you're not using Jest, Vitest or Jasmine, we do not have special syntax support but you can of course still use ArchUnitTS. Please read here.

🎬 Demo

https://github.com/user-attachments/assets/426f7b47-5157-4e92-98a3-f5ab4f7a388a

🐹 Use Cases

Many common uses cases are covered in our examples folder. Note that they are not fully working repositories but code snippets. Here is an overview.

Layered Architecture:

Micro Frontends:

Clean Architecture:

Hexagonal Architecture:

Angular Example:

🐲 Example Repositories

Here are a few repositories with fully functioning examples that use ArchUnitTS to ensure architectural rules:

🐣 Features

This is an overview of what you can do with ArchUnitTS.

Circular Dependencies

it('services should be free of cycles', async () => {
  const rule = projectFiles().inFolder('src/services/**').should().haveNoCycles();
  await expect(rule).toPassAsync();
});

Layer Dependencies

it('should respect clean architecture layers', async () => {
  const rule = projectFiles()
    .inFolder('src/presentation/**')
    .shouldNot()
    .dependOnFiles()
    .inFolder('src/database/**');
  await expect(rule).toPassAsync();
});

it('business layer should not depend on presentation', async () => {
  const rule = projectFiles()
    .inFolder('src/business/**')
    .shouldNot()
    .dependOnFiles()
    .inFolder('src/presentation/**');
  await expect(rule).toPassAsync();
});

Naming Conventions

it('should follow naming patterns', async () => {
  const rule = projectFiles()
    .inFolder('src/services/**')
    .should()
    .haveName('*-service.ts'); // my-service.ts for example
  await expect(rule).toPassAsync();
});

it('components should be PascalCase', async () => {
  const rule = projectFiles()
    .inFolder('src/components/**')
    .should()
    .haveName(/^[A-Z][a-zA-Z]*Commponent\.ts$/); // MyComponent.ts for example
  await expect(rule).toPassAsync();
});

Code Metrics

it('should not contain too large files', async () => {
  const rule = metrics().count().linesOfCode().shouldBeBelow(1000);
  await expect(rule).toPassAsync();
});

it('should have high class cohesion', async () => {
  const rule = metrics().lcom().lcom96b().shouldBeBelow(0.3);
  await expect(rule).toPassAsync();
});

it('should count methods per class', async () => {
  const rule = metrics().count().methodCount().shouldBeBelow(20);
  await expect(rule).toPassAsync();
});

it('should limit statements per file', async () => {
  const rule = metrics().count().statements().shouldBeBelowOrEqual(100);
  await expect(rule).toPassAsync();
});

it('should have 3 fields per Data class', async () => {
  const rule = metrics()
    .forClassesMatching(/.*Data.*/)
    .count()
    .fieldCount()
    .shouldBe(3);

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

Distance Metrics

it('should maintain proper coupling', async () => {
  const rule = metrics().distance().couplingFactor().shouldBeBelow(0.5);
  await expect(rule).toPassAsync();
});

it('should stay close to main sequence', async () => {
  const rule = metrics().distance().distanceFromMainSequence().shouldBeBelow(0.3);
  await expect(rule).toPassAsync();
});

Custom Rules

You can define your own custom rules.

const ruleDesc = 'TypeScript files should export functionality';
const myCustomRule = (file: FileInfo) => {
  // TypeScript files should contain export statements
  return file.content.includes('export');
};

const violations = await projectFiles()
  .withName('*.ts') // all ts files
  .should()
  .adhereTo(myCustomRule, ruleDesc)
  .check();

expect(violations).toStrictEqual([]);

Custom Metrics

You can define your own metrics as well.

it('should have a nice method field ratio', async () => {
  const rule = metri
View on GitHub
GitHub Stars366
CategoryDevelopment
Updated14m ago
Forks11

Languages

TypeScript

Security Score

100/100

Audited on Apr 3, 2026

No findings