# Apps and surfaces Two surfaces, **one process, one port, one database**: ``` example.com/ ──┤ apps/frontend │ React SPA, talks to /api example.com/api/thing ──┤ servers/api │ serializers -> JSON └───────┬─────────┘ │ ┌───────┴─────────┐ │ appdb │ queries └─────────────────┘ ``` Here the SPA is a client of the API like any other, which is the point of a SPA. The rule below is about the server side: the API is the only door to the data, and it must never return HTML. ## HTML and JSON are siblings, not layers ## The rule **Both surfaces call the query layer. Neither calls the other.** ### The web app does not call its own API It runs in the same process. Going out over HTTP to talk to ourselves buys a round trip, a serialisation, a deserialisation, and a second way for two screens to disagree. It also makes every page render depend on the server being able to reach itself, which is a failure mode nobody wants to debug. Import the query function. That is what it is for. ### The API does not return HTML Not even for htmx. It is tempting — the fragment is right there — but the API is a documented, external contract consumed by things that are not this website, and htmx's needs are not theirs. A fragment that becomes a de facto API cannot be changed without breaking someone, and you find out when they tell you. Fragments belong to `apps/web` and are free to change on any afternoon, precisely because nothing outside this repo depends on them. ## What they share, and what they must not | | | |---|---| | **Shared** | query functions, domain rules, database types | | **Not shared** | serialisation, error presentation, validation of transport, URL shape | The API has serializers because JSON needs a stable, named, snake_case shape. The web app has templates because HTML needs markup. Trying to make one produce the other gives you a serializer with markup in it. ## They are allowed to behave differently This is the part that feels wrong and is right. The two surfaces have different callers, and the same event can deserve different answers: | Situation | `/api` | the site | |---|---|---| | Creating something whose name is taken | **409** with an error envelope | show the list, with the existing one in it | | A malformed id | **404**, in the envelope | render the page's empty state | | Something failed | error object with a `type` a program can branch on | a sentence a person can read | A program needs to distinguish "already exists" from "created". A human clicking Create wants the thing they asked for to be on screen; being shown an error for a collection that exists is a worse answer than showing it to them. **What must never diverge is the query layer.** If the two surfaces can produce different *data* for the same request, that is a bug. If they present the same data differently, that is the point. ## Naming `/thing` and `/api/thing` stay parallel. `/api` is mounted before the site's catch-all in `src/server.ts`, so it is resolved first and the site never swallows an API path. Within the site, pages and partials are **different URLs**, never the same URL switching on `HX-Request` — see `docs/WEB.md`. Combined with the `/api` split, that gives three URL spaces with one representation each, and every one of them is cacheable without a `Vary` header. ## Adding a resource 1. `app_db/src//` — schema and queries. Both surfaces will import these. 2. `servers/api/.ts` — a router, plus a serializer in the shared serializer module. 3. `apps/web//partials.tsx` and a page — both calling the same queries. 4. Mount the API router in `servers/hono.ts`; add the site's routes in `apps/web/index.tsx`. If step 3 finds itself wanting something step 2 already computed, that logic belongs in step 1.