Make the next app as much of a CLI app as possible. Feature work happens in the terminal, in-process, against the real development database. The HTTP API is a thin adapter you add afterwards, and a lint rule fails CI if it ever gets a route the CLI does not have.
Author: James Phoenix | Date: September 2026
The API Is the Wrong Place to Develop
Most web apps grow from the HTTP layer outwards. You write a route, you write a handler, the handler reaches into the database, and the feature exists. To exercise it you start a server, open a client, craft a request, and read a response. That loop was tolerable when a human was the only developer. It is a bad loop for an agent, and it is a bad loop for me now that agents write most of the first draft.
The cost is not the HTTP round trip. The cost is everything a running server hides. Process boundaries eat stack traces. Serialisation flattens types. Auth middleware sits between me and the function I actually want to call. Reproducing a bug means reconstructing a request instead of calling a use case with the arguments that broke it. Every layer between the developer and the domain is a layer the agent has to guess through.
So I am inverting the default for the next app. The CLI is the primary development interface. It runs the application in-process against the real dev Postgres and Redis. Features are built and validated there first. The API arrives later as a second transport over the same code, and it is not allowed to know anything the CLI does not.
The Dependency Flow
The layering is the part that makes the inversion cheap rather than heroic. One direction of dependency, four packages, two apps:
packages/infra (Postgres, Redis, external clients)
↓
packages/domains (models, invariants, pure domain logic)
↓
packages/application (use cases and orchestration)
↓
apps/api (HTTP transport over application)
apps/cli (CLI transport over application)
The API and the CLI are siblings, not parent and child. Both are adapters. Both parse transport input, invoke an application-layer use case, and translate the result for their medium. Neither contains business logic, because the moment one does, the other cannot reach it. This is the same pure-decision-then-effectful-orchestration split I described in DDD, FP, and Event-Driven Architecture, with one addition: the application layer is the shared boundary, and every transport sits above it or does not exist.
Three imports are forbidden, and they are the three that every codebase drifts towards when nobody is watching:
API → domain logic (business rules in a handler)
API → DB (a query in a route)
API → Redis (a cache read in middleware)
Preferred, and lint-enforced:
API → Application → Domain → Infra
CLI → Application → Domain → Infra
The Development Loop
With the layering in place the loop becomes CLI → Application → Domain → Infra → DB/Redis, with nothing else in the chain. I use the CLI to create and modify behaviour, exercise use cases, inspect application state, seed and mutate dev data, reproduce bugs, and validate a feature before it gets an HTTP route. No server process. No client. One command, one use case, one result printed as JSON.
This matters most for the agent. A coding agent driving a CLI gets a synchronous exit code, structured output, and a stack trace that points at the application layer rather than at an HTTP framework. It can seed a fixture, run the use case, assert on the row, and tear down, all inside a single Bash call. It does not need to know how to start the server, wait for a port, or construct a bearer token. The CLI turns every feature into something an agent can drive end to end without leaving the terminal. That is the whole reason to do this. The human ergonomics are a side effect.
The same property is why Make Your Stack Headless ranks click-only capabilities by how often they stall an agent. This note is the design-time version of that audit. Instead of retrofitting a scriptable path after the GUI-only one bites, the scriptable path is the first one built, and the GUI-shaped transport has to earn its place afterwards.
Parity Is a Lint Rule, Not a Convention
The rule that keeps the two transports honest is blunt: every API route must have a corresponding CLI command, and a change that adds one without the other fails CI.
I do not trust this to code review. Review catches the first violation and misses the fifth, and an agent producing a route at two in the morning has no reviewer at all. I trust it to a structural lint of the kind I described in ESLint Rules Are Programs: read the route registry, read the CLI command registry, diff the two sets, and report each missing pair with the file and line where the route was declared. Same shape as enforce-surface-parity there, narrowed to exactly two surfaces. In the six-layer lint harness this sits in the structural layer, next to table-to-factory parity, because it is the same kind of invariant: two registries that must stay in step.
Two smaller rules complete it. One forbids apps/api from importing anything in packages/domains or packages/infra directly, so a handler physically cannot become fat. The other requires every use case exported from packages/application to be reachable from at least one CLI command, which is the same parity check from the other direction and catches the case where a use case exists but no transport exposes it.
What Goes in CLAUDE.md
The layering only compounds if the agent knows it. This is the paragraph I put in both CLAUDE.md and AGENTS.md, close to verbatim:
Drive application development primarily through the CLI, in-process against the
dev database and Redis. Implement and validate each feature in apps/cli before
exposing it in apps/api. Both apps are thin adapters over packages/application;
never put business logic, DB access, or Redis access in a transport. Every API
route must have a matching CLI command; the parity lint fails CI if it does not.
Short, structural, and paired with a rule that fails when it is ignored. That pairing is what turns an instruction into a constraint. The agent reads the paragraph, drifts anyway, and the lint hands the drift back to it with a file and a line.
Where It Fails
The failure mode is the one I already hit and wrote up in The Same Endpoint Now Has Three Callers. When validation lives at the HTTP decode edge and a second caller bypasses HTTP, the second caller skips the checks. With CLI-first the risk flips: validation that exists only in a CLI argument parser is invisible to the API. The answer is the same either way. Validation belongs in the application layer, below both transports, or it does not exist for one of them. Argument parsers and request schemas decode shape. Use cases enforce bounds.
The second limit is honest scope. Some behaviour is genuinely transport-specific: streaming responses, websocket fan-out, file uploads with multipart boundaries. Those get an API route with a CLI equivalent that reads from stdin or writes to a path, and the parity rule accepts an explicit // cli-parity: streaming marker that names the CLI command covering it. What the rule does not accept is silence.
One Sentence
Build the next app as a CLI over a shared application layer, do all feature work in the terminal against the real dev database, add the HTTP API as a sibling adapter afterwards, and let a lint rule rather than a reviewer prove the two transports never diverge.
Related
- Make Your Stack Headless So Agents Can Drive It - The retrofit audit this note replaces with a design-time default
- DDD, FP, and Event-Driven Architecture - The pure domain / effectful application split the layering depends on
- ESLint Rules Are Programs - The surface-parity rule that enforces API and CLI stay in step
- The Same Endpoint Now Has Three Callers - What breaks when validation lives in one transport
- Rewrite Your CLI for AI Agents - How the CLI surface should be shaped once it is primary
- Agent-Native Architecture - The parity principle in its general form
- The Six-Layer Lint Harness - Where the parity lint sits in the wider rule stack
- The MCP Abstraction Tax - Why the CLI comes before any protocol wrapper

