Why the code is arranged the way it is, and what each boundary buys.
The shape
The contracts package is the load-bearing part. Both sides import it, so a change
to a request or response shape is a compile error on both sides at once, rather
than a disagreement discovered in production.
Why hexagonal on the API
Each feature module is arranged as:
Dependencies point inward. A use case declares what it needs as a port; the
infrastructure layer implements it and the module wires the two together in one
place. Three things follow:
- The domain is testable without a database. Segment matching and campaign
status rules are pure functions over their own types.
- The seed script and a future job reuse the use cases, not a parallel
implementation.
customers.import is the same code whether a person or a
scheduled task triggers it.
- Framework types stop at the controller. A
Repository from TypeORM never
reaches a use case, so swapping persistence does not reach into business rules.
Where composition happens
app.module.ts is the only place that knows every module exists. A feature module
never imports another, which is what keeps the dependency graph acyclic and
readable.
Why the contract is generated into OpenAPI
The alternative — @nestjs/swagger decorators on controllers — describes the same
facts a second time, in a place the compiler does not check against the client.
The two inevitably disagree. Generating the specification from the registry means
the published reference cannot describe an endpoint that does not exist, or omit a
field the API accepts.
The rendered UI is Swagger UI, served from the installed package rather than a
CDN: the reference has to work on an isolated network, and a third party must not
be able to change what a reader is shown. See
the shared contract.
Why the version is in the path
/api/v1/customers rather than a header, a media type, or no version at all. See
versioning.
Why responses are validated on arrival
The client validates every response against the contract before handing it to the
application. This costs a parse per response and buys something specific: a
deployment mismatch — a new API version, a proxy rewriting a body, a stale cached
bundle — fails loudly at the boundary instead of producing undefined three
layers into a component.
Loud and early is the intent. A silent cast here would move the failure to the one
place that cannot explain it.
Why the model runs server-side
The browser cannot hold a provider key, and a prompt is business logic that
belongs under version control. The API owns a model port with four
implementations: the Anthropic adapter, the Gemini adapter, the OpenCode Zen
adapter, and a deterministic local generator. One decision made at boot binds
either port, so tests and development are offline and repeatable while production
gets a hosted model. Which adapter is bound comes from configuration alone —
adding a key is the only step needed to change models.
See the generative pipeline, and
add an LLM provider for the steps a new provider
follows.
Why state is split between Zustand and TanStack Query
They answer different questions.
- Zustand owns client state that no request produces: whether a session
exists, which tokens are held. The HTTP client reads it outside React, because
it needs a token on every call.
- TanStack Query owns anything that came from the server: customers,
segments, campaigns, KPIs.
The practical consequence is that a write invalidates a cache rather than
notifying a component. Importing customers refreshes the table, the KPIs and the
dashboard because all three read the same keys — with no event bus to keep in
sync. See client state.
The one thing that is neither is a form draft: local component state, unsaved and
unshared.
What is deliberately absent
- A service layer between controller and use case. It would only forward
arguments.
- Interfaces generated by hand for responses. The registry entry is the type.
- A mock database in the API test suite. The port is faked at the test
boundary instead, so tests stay fast without pretending persistence is
optional.
Last modified on October 6, 2026