Skip to content

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.