CAITSGet started

Clean Architecture starter for TypeScript

Code that stays clean. Whoever writes it.

CAITS is a starter and a base library for full-stack TypeScript apps on Marko and Vite. It sets up Clean Architecture, and one command, npm run check, proves that each change follows it. When you, a teammate or an AI agent breaks a rule, the check fails, and names the file and the rule. Your agent reads the error and fixes its own work.

npx @ingenioz-it/caits init my-app --example
  • Node 22.18, 24 or 26+
  • Linux, macOS or WSL 2
  • Green from the first run
Clean Architecture In TypeScript

All checks passed

Built on TypeScript Marko Vite Vitest Stryker Playwright ESLint

Why CAITS

Every codebase starts clean.

Then come the deadlines, the new teammates and the AI agents. One shortcut at a time, the architecture erodes, the tests stop proving anything, and every change gets slower and riskier.

  • Rules that live in a wiki

    The architecture is a diagram that nobody checks. Code that breaks it compiles, slips through a busy review, and stays.

  • Tests that check nothing

    Coverage says 90%. But a test without a real assertion runs the lines without checking them. The bug ships anyway.

  • Agents that improvise

    An agent writes in minutes what would take you a day. Without hard rules, each session invents its own structure, and you review every line.

A rule that no tool checks is a wish. CAITS turns your rules into checks.

The harness

Six checks. One command. Green means every rule held.

  • Types Strict TypeScript, Marko templates included.
  • Lint One style for all the code.
  • Architecture 52 rules: each file in its layer, each import in the right direction.
  • Unit tests 100% coverage, in seconds.
  • Mutants 100% killed: a test that checks nothing gets caught.
  • End-to-end In a real browser, on the production build.

~/my-app

npm run check
6 checks at once: typecheck, lint:code, lint:architecture, test:coverage, test:mutation, test:e2e
✓ lint:architecture  0s
✓ lint:code          4s
✓ typecheck          5s
✓ test:coverage      5s
✓ test:e2e           9s
✓ test:mutation      53s
All 6 checks passed in 53s.

When a rule breaks, you read this.

Here, a use case imported an adapter instead of its port. The check names the file, the import and the rule, and nothing else.

Run npm run check before each commit, or let your agent run it: a check that passes takes one line, and only the checks that fail print their output.

npx caits architecture

src/backend/messages/application/command/add-message/add-message.handler.ts → src/backend/messages/infra/message-repository/file-message-repository.js: application must not depend on infra (layer-direction)

1 architecture violation.

Coverage says the lines ran. Mutants say they're checked.

Stryker changes your code on purpose: a > becomes >=, a condition turns true. Each change is a mutant. If your tests still pass, the mutant survives: you've found a line that no test really checks.

For this run, one test was removed from the example app, and Stryker ran on Message.ts only. The coverage stayed at 100%, but no test sent a message of exactly 280 characters any more, and this mutant survived. Put the test back, and it's dead. CAITS asks for every mutant to be killed: a test that checks nothing has nowhere to hide.

npx caits mutation-summary --survivors

Mutation score: 91.7%  (11 killed of 12 valid mutants)

  92%  src/backend/messages/domain/entity/Message.ts  (killed 11, survived 1, no coverage 0)
        L16 [survived] EqualityOperator: if ([...trimmed].length > Message.MAX_LENGTH) throw new InvalidMessageError(`A message has  →  [...trimmed].length >= Message.MAX_LENGTH

With an AI agent

Your agent writes. The harness checks.

Coding agents are fast, confident, and they guess. CAITS replaces the guessing with rules they can't skip, and with errors that tell them how to fix their own work.

  1. You ask for a feature, in one sentence.
  2. The agent writes the code and its tests, in the same shape as the rest.
  3. The harness fails, and names the file and the rule.
  4. The agent fixes its work, until every check is green.
  5. You review a change that already passes every check.
  • Less to read

    Small files, one shape everywhere, and an AGENTS.md that gives the rules in 28 lines. The agent reads a handful of files, not the whole project.

  • No odd decisions

    Where each file goes, what it may import, how it is tested: the rules have decided. An agent that improvises fails the check, and reads there what to fix. It corrects itself, without you.

  • Work you can check

    Strict types, 100% coverage, 100% of the mutants killed: the agent's code passes the same harness as yours. Green means it meets every rule you set.

A prompt to start with

Add orders next to messages, in the backend and the frontend, with the use case place-order. Follow the shape of messages. Run npm run check until it passes.
How CAITS guides AI agents

The difference

A starter gives you folders. CAITS keeps them in order.

A typical starterCAITS
ArchitectureA typical starter: Folders, and conventions in a READMECAITS: 52 rules, in each run of the check
TestsA typical starter: A coverage percentageCAITS: 100% coverage, and 100% of the mutants killed
ToolingA typical starter: Seven tools to set up and keep in syncCAITS: Set up by one command, yours to override
A year laterA typical starter: Up to each teamCAITS: The same shape, the same checks
AI agentsA typical starter: Guess the conventionsCAITS: Read AGENTS.md, fail fast, fix their work

No debates

Where each file goes is already decided.

Clean Architecture and Domain-Driven Design, without the meetings. Your business lives in bounded contexts. Each context has the same four layers, and every dependency points to the domain. Learn one context, and you know them all.

src/backend/messages

domain/          # the business: entities, errors, ports
  entity/Message.ts
  port/message-repository/
    MessageRepository.ts
    MessageRepository.contract.ts
application/     # one folder per use case
  command/add-message/
    add-message.dto.ts
    add-message.handler.ts
    add-message.test.ts
infra/           # the adapters of the ports
  message-repository/
    file-message-repository.ts
    in-memory-message-repository.ts
presentation/    # HTTP in, HTTP out
  add-message-http/
di.ts            # the bindings, read by CAITS
  1. Domain

    The business rules, in plain TypeScript classes: no Node API, no database, no view. Only three small types of CAITS.

  2. Application

    One use case per folder: its DTOs, its handler, its test. It talks to the world through ports.

  3. Infra

    The adapters of the ports. Each one runs the contract of its port, so the fake and the real one agree.

  4. Presentation

    Controllers in the backend; views and stores in the frontend. The only layer that answers requests and renders Marko.

Explore the architecture and its 52 rules

In the box

Everything a full-stack app needs. Nothing it doesn't.

  • Dependency injection

    Each context lists its bindings in a di.ts. CAITS finds the contexts and builds their classes.

  • Message buses

    Commands and queries go through the bus of their context. Events cross contexts through the event bus.

  • Frontend stores

    Views publish events. The store turns them into commands, and keeps the state that the views follow.

  • HTML first

    Pages arrive as ready HTML, and JavaScript ships only where a page reacts. This page: 11 kB of JavaScript, 5.5 kB compressed, for its copy buttons and the count of its visits.

  • Contract tests

    Each port has a contract, and each adapter runs it. The in-memory adapter of your tests behaves like the real one.

  • A test helper per layer

    Use case scenarios, HTTP controllers, views, adapters: each layer has its test helper. The end-to-end tests run Playwright, set up for you.

  • Living documentation

    Your tests become an HTML report of what the app does. A test named “Given …, When …, Then …” shows as three steps.

  • Yours to tune

    Every tool reads your configuration last. caits update refreshes the defaults, never your changes.

Get started

From zero to green in one command.

  1. 5 min

    Create your app 

    One command writes the project, installs it, and leaves it passing every check.

  2. 10 min

    Follow one message 

    Two diagrams follow a message of the example app from the click to the file, and back, step by step.

  3. 20 min

    Write your first use case 

    A tutorial, test first, with npm run tdd running: from the DTO to the HTTP route, until every check is green.

Questions

Questions before you try it.

Another question? Ask it on GitHub 

What is CAITS, exactly?

A starter and a base library for full-stack TypeScript apps on Marko and Vite. init writes the project, and the package stays under it: a dependency container, message buses, frontend stores, a Vite plugin, test helpers, and the configuration of every quality tool. Your project holds the business code. CAITS holds none.

Is it a template, or a framework?

A bit of both. The files that init writes are a template: they become your code. The package stays a dev dependency, @ingenioz-it/caits from npm, and your domain and your use cases are plain TypeScript classes that import almost nothing from it.

Why Marko, and not React?

Marko streams ready HTML from the server and ships JavaScript only for the parts of a page that react. Its templates are type-checked like the rest of the code. And in CAITS, Marko stays in the presentation layer: the domain and the use cases are plain TypeScript, free of any view technology.

Aren't 100% coverage and a 100% mutation score too strict?

They're the default, not a dogma. With small files and tests written first, they're within reach: the example app and this website both reach them. When a mutant can't be killed because it changes nothing, Stryker lets you disable it on its line, with a comment that gives the reason, and the comment shows in the review. The thresholds live in your quality/ folder, too.

How long does a full check take?

The six checks run at the same time, and the mutation tests take the longest: Stryker runs your unit tests once for each mutant that compiles: 191 of the 387 mutants of the example app. On a machine with 32 threads, the whole check takes 53 seconds; with 8 workers, the mutation tests alone take 78 seconds, and about two minutes with 4. The time grows with your code. While you work, npm run tdd reruns only the tests of what you change; run npm run check before you push.

Can I change the rules?

Yes. Each file of quality/ imports CAITS's defaults, then changes them: remove a rule, add your own, adjust a threshold. Your file has the last word, and caits update never touches it.

Can I add CAITS to an existing project?

Yes, if it's a Marko app, or nearly empty: init adds what's missing. It keeps each file and each value that the project has, except quality/caits/, which CAITS owns. In a project with another structure, the architecture check fails until the code moves into contexts. CAITS fits a new app best.

Can I leave CAITS later?

Yes. Your domain imports only three small types from CAITS: Result, Token and DomainEvent. The rest of your code uses its container, its buses and its controllers. To leave, copy the code of CAITS that you use into your project (its license, 0BSD, asks for nothing), or replace it with your own.

Which AI agents does it work with?

Any agent that can run a shell command, such as Claude Code, Codex, Cursor, GitHub Copilot, Gemini CLI or Aider. The harness is plain npm scripts, and the rules come in an AGENTS.md file. Claude Code reads CLAUDE.md: one line, @AGENTS.md, points it there.

Is it ready for production?

Not battle-tested yet. CAITS 0.5 runs this website and the example app, and has one maintainer, a software craftsman since 2009. Expect breaking changes before 1.0. CAITS is the package @ingenioz-it/caits on npm: npm update gets the fixes of your version. For a new version, run npm install --save-dev @ingenioz-it/caits@latest, then npx caits update, and review the diff of quality/caits/.

Can I use a database?

Yes. The example keeps its messages in a JSON file, through a port: MessageRepository. For PostgreSQL or any other database, write another adapter of the same port, and run the contract of the port on it. When the contract passes, the use cases work with it, unchanged.

How do I deploy it?

Run npm run build, then npm start (node dist/index.mjs) on any host with Node.js 22 (22.18 or later), 24, or 26 and later. PORT sets the port, and the example reads DATA_DIR for the folder of its files.

When is CAITS not a good fit?

When you need React or Next.js and their ecosystem; when you work on native Windows (WSL 2 works); when you need a stable API today, since breaking changes will come before 1.0; when a minute or two of mutation tests in each full check is too much; or for a large existing codebase with another structure, which would fail most of the rules.

What do I need?

Linux, macOS, or WSL 2 on Windows (native Windows does not work); Node.js 22 (22.18 or later), 24, or 26 and later; and git. The command installs everything else, the browser for the end-to-end tests included.

Start clean. Stay clean.

A few minutes from now, your app passes every check. Keep it that way, one green run at a time.

npx @ingenioz-it/caits init my-app --example

This website is a CAITS project too. It passes the same six checks, every mutant killed. So does the example app : clone it and run npm run check yourself.