ADR 0003 — API / Frontend separation¶
- Status: Accepted
- Date: 2026-04-27
- Decision drivers: independent deployability of API and SPA, no backend URLs leaking into user-visible artifacts, localization lives in one place
- Resolves: Open Question 3 in
wiki/plans/login.md
Context¶
Molib comprises a .NET API (Molib.Api) and a React/MUI SPA (frontend). End users interact with the SPA only — never directly with the API. We need to commit to that boundary so we don't drift into the classical server-rendered shape where backend routes leak into emails, browser history, and support tickets.
The immediate trigger: how the verification-email link in sign-up should behave. Two common patterns:
- A — link points at the API (
/users/verify/{token}); API marks the user verified and redirects to the SPA. - B — link points at the SPA; SPA reads the token from its own URL and POSTs to the API.
Option A couples user-facing UX to backend routes (a UX change requires a backend deploy), forces the API to know about layout concerns (where to redirect, how to phrase errors), and exposes backend URLs in user-visible artifacts.
Decision¶
The .NET API (Molib.Api) is a pure HTTP/JSON API. The SPA mediates every user-facing flow.
Concretely:
- No HTML, no Razor views, no MVC view rendering by the API. Endpoints return JSON or empty bodies with appropriate status codes.
- No redirects from API endpoints to frontend URLs. API responses are data; the SPA decides where to navigate.
- End-user links — email links, share links, deep links — point at the SPA, never at the API. The SPA extracts any token / context from its own URL and calls the API.
- The API never composes user-facing copy. Error responses use a stable machine-readable shape (e.g.
{ "error": "EmailNotVerified" }); the SPA maps these to localized Portuguese strings (per project language policy).
Verification flow as the canonical example¶
1. User signs up via SPA → SPA calls POST /users → API returns 201 + verification token.
2. (Future) An email is sent with a link to the SPA, e.g.
https://app.molib.example/verify?token=ABC123
3. User clicks link → loads the SPA route /verify.
4. SPA reads token from URL → POST /users/verify { token }.
5. API returns 204 (verified) or 4xx with a stable error code.
6. SPA renders success / "token expired" / "already verified" — user never sees a backend URL.
The same pattern applies to future flows: password reset, magic-link sign-in, share invitations.
Consequences¶
Positive:
- API is independently deployable and reusable (mobile clients, integration tests, possible future public API).
- Frontend UX changes don't require backend deploys.
- Backend URLs never appear in emails, browser history, screenshots, or support tickets.
- Localization lives only in the frontend i18n bundle. The API stays language-agnostic.
- The API can be exercised purely via JSON in tests without rendering anything.
Negative / things to watch:
- An extra round-trip on flows that could otherwise be a single click→redirect (e.g. verification). Acceptable.
- The SPA must handle every error state — there is no "fallback page" produced by the API.
- Email templates (when added) must construct frontend URLs, not API URLs. The frontend's public base URL must be available to whatever code sends emails (env var, e.g.
MOLIB_PUBLIC_URL).
Out of scope of this rule¶
This ADR governs end-user flows. Internal / operator-facing surfaces are not subject to it:
- Background-job dashboards (e.g. Hangfire
/hangfire) — operator-only admin UI, not linked from any user flow. - Health and readiness endpoints (
/healthz,/readyz). - OpenAPI / Swagger UI if introduced — developer-facing, not user-facing.
These surfaces may render HTML and may live on the same Molib.Api host. They must, however, be access-controlled so they are not reachable by end users in non-dev environments.
Triggers for revisiting¶
- A non-SPA client (e.g. server-rendered email preview, an embedded help center) genuinely needs HTML from the API. Even then the right answer is usually a separate small service, not loosening this rule.