Server-rendered vs API + SPA vs server components
Three fundamentally different ways to build a web UI — each with its own backend implications for data, routing, and deployment.
Classic SSR architecture: HTTP request → controller → DB query → template engine → HTML string → response. Each user action is typically a full page load; sessions in cookies, auth via server-side redirects. Pros: minimal JS shipped, trivial SEO (Google gets complete HTML), accessible by default. Cons: every interaction is a round-trip unless you add client-side JS.
HTMX / Hotwire / Turbo renaissance: partial HTML swaps over AJAX replace full-page reloads. <button hx-post="/like" hx-target="#count" hx-swap="innerHTML">Like</button> — server returns an HTML fragment, browser swaps it in. SPA-like feel, 10 KB of JS, server owns rendering. Adopted heavily in Rails (Hotwire default in Rails 7), Django, Phoenix, FastAPI. Real production viable alternative to React for many product shapes.
API + SPA architecture — tier separation: the API owns domain, auth, DB. The SPA owns UI state, client routing, and fetch orchestration. Shared contracts: OpenAPI (code-gen clients), GraphQL (schema-first), tRPC (type-safe for TS-to-TS). Costs: (a) two deployment pipelines + versioning; (b) CORS config that everyone gets wrong once; (c) auth token lifecycle (cookies with SameSite vs JWT in Authorization header — each with its own security trade-offs); (d) duplicated validation (zod on client, Pydantic on server) unless you use tRPC or OpenAPI codegen. Benefits: ONE API serves web + iOS + Android + third-party integrations.
React Server Components (RSC): a new rendering model where server components execute ONLY on the server — they can await a DB directly, import server-only libs (AWS SDK, fs), and never ship their code to the browser. Client components ("use client" directive) run in the browser for interactivity (events, state, effects). Next.js streams the server-rendered output to the client, which hydrates only client-marked parts. Zero JS shipped for static / server-only UI.
Server Actions: the mutation counterpart. Client forms call typed server functions — async function submit(formData) { "use server" ... } — and the framework handles serialization, invocation, and cache revalidation. Replaces manually defining POST/PUT routes + client fetch wrappers for first-party UI. Type-safe RPC-feel. Breaks any 'separation of frontend/backend' purism — deliberately.
RSC + Server Actions limitations: (a) tightly coupled to the framework (Next.js) and its deployment model (Vercel + serverless or custom Node server); (b) you can NOT reuse these for non-web clients — mobile still needs a separate API; (c) new mental model, tooling still maturing, edge cases in caching semantics (revalidatePath, revalidateTag); (d) harder to version and expose publicly — server actions are not a stable API contract.
Decision heuristic: (i) public content / SEO-heavy / marketing site → classic SSR or Next.js (both excellent SEO). (ii) complex web app + native mobile → API + SPA + mobile clients (one backend, three clients). (iii) web-first product with rich UI and heavy type safety → Next.js/Remix + RSC (velocity for single-client products). (iv) internal tool or CRUD-heavy admin → Django/Rails SSR + HTMX, or Retool-style for maximum speed.
Grounded on https://react.dev/reference/rsc/server-components
Next up
Stateless services & horizontal scaling
To scale horizontally, any request must be servable by any instance. In-memory state breaks this — sessions, caches, and uploads must live outside the process.