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
All checks passed
- 52 architecture rules, in each check
- 100% coverage, required of your tests
- 100% of the mutants killed, required too
- 1 command runs all six checks
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_LENGTHWith 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.
- You ask for a feature, in one sentence.
- The agent writes the code and its tests, in the same shape as the rest.
- The harness fails, and names the file and the rule.
- The agent fixes its work, until every check is green.
- You review a change that already passes every check.
Less to read
Small files, one shape everywhere, and an
AGENTS.mdthat 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
Addordersnext tomessages, in the backend and the frontend, with the use caseplace-order. Follow the shape ofmessages. Runnpm run checkuntil it passes.
The difference
A starter gives you folders. CAITS keeps them in order.
| A typical starter | CAITS | |
|---|---|---|
| Architecture | A typical starter: Folders, and conventions in a README | CAITS: 52 rules, in each run of the check |
| Tests | A typical starter: A coverage percentage | CAITS: 100% coverage, and 100% of the mutants killed |
| Tooling | A typical starter: Seven tools to set up and keep in sync | CAITS: Set up by one command, yours to override |
| A year later | A typical starter: Up to each team | CAITS: The same shape, the same checks |
| AI agents | A typical starter: Guess the conventions | CAITS: 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 CAITSDomain
The business rules, in plain TypeScript classes: no Node API, no database, no view. Only three small types of CAITS.
Application
One use case per folder: its DTOs, its handler, its test. It talks to the world through ports.
Infra
The adapters of the ports. Each one runs the contract of its port, so the fake and the real one agree.
Presentation
Controllers in the backend; views and stores in the frontend. The only layer that answers requests and renders Marko.
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 updaterefreshes the defaults, never your changes.
Get started
From zero to green in one command.
- 5 min
Create your app
One command writes the project, installs it, and leaves it passing every check.
- 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.
- 20 min
Write your first use case
A tutorial, test first, with
npm run tddrunning: from the DTO to the HTTP route, until every check is green.
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 --exampleThis 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.