Skip to content

Build a UI for a Winter Boot API

Winter Boot has no view layer — no templates, no server-rendered pages — and that is deliberate. It is a framework for backend services of all kinds: REST APIs, background workers, scheduled jobs, daemon processes, and message consumers. The parts that face a browser expose JSON APIs; the UI itself lives in a separate frontend application built with React, Next.js, Vue, Svelte, or plain HTML with Tailwind/Bootstrap, talking to those APIs over HTTP. This guide shows the split and the minimal wiring on each side.

browser ── static files (CDN / Nginx) ──► your SPA (React, Next.js, …)
│
└── /api/* ── reverse proxy ──► Winter Boot (JSON only)

One origin serves both the static bundle and the API (the proxy forwards /api/* to the Swoole port). Same-origin means no CORS configuration, and the session cookie from the RBAC guide just works. The framework ships no CORS module, so treat cross-origin browser calls as a problem to avoid, not to solve.

Winter Boot owns

Routes, JSON contracts, authentication, RBAC/ABAC decisions, transactions.

The frontend owns

Pages, styling, form state, loading and error states, navigation.

The frontend depends on a contract, not on PHP. Fix these three shapes and both sides can evolve independently:

POST /api/login — request
{ "username": "alice", "password": "s3cret" }
POST /api/login — response (200, sets the SID cookie)
{ "success": true, "data": { "username": "alice" } }
Guarded endpoints — errors
{ "error": "Authentication required" }
{ "error": "Forbidden" }

Conventions that keep frontend code simple:

  • Every success body wraps in { "success": true, "data": … }; every failure carries { "error": "human-readable reason" } with the right status (401 unknown session, 403 missing grant, 400 bad input).
  • Put the API under a prefix (context-path: /api in application.yml or a proxy rule) so the proxy can distinguish API traffic from static files.
  • Write the backend endpoints with REST controllers and guard them exactly as the RBAC and ABAC guides show — the frontend below logs in with AuthService sessions and calls guarded routes unchanged.

A tiny React app is enough to show the whole pattern: one fetch wrapper that sends the session cookie and translates statuses, one component with login and a guarded list, and a dev proxy so the browser stays same-origin. Switch between the three files — the same calls work identically from Next.js, Vue, or Svelte:

The only place that knows HTTP. Same-origin base URL, cookies included, 401/403 surfaced as typed errors the UI switches on.

const BASE = '/api';
export class AuthError extends Error {}
export class ForbiddenError extends Error {}
async function request(path, options = {}) {
const res = await fetch(BASE + path, {
credentials: 'include', // send the SID session cookie
headers: { 'Content-Type': 'application/json', ...(options.headers ?? {}) },
...options,
});
if (res.status === 401) throw new AuthError('Please log in again.');
if (res.status === 403) throw new ForbiddenError('You are not allowed to do that.');
const body = await res.json().catch(() => ({}));
if (!res.ok) throw new Error(body.error ?? `Request failed (${res.status})`);
return body.data ?? body;
}
export const api = {
login: (username, password) =>
request('/login', { method: 'POST', body: JSON.stringify({ username, password }) }),
logout: () => request('/logout', { method: 'POST' }),
pendingOrders: () => request('/orders/pending'),
};

Styling is ordinary frontend work: the classNames above are Tailwind, but a Bootstrap stylesheet with class attributes behaves the same — the backend never sees it.

  • Build the SPA (npm run build) and serve the static files from Nginx or a CDN — never from the Swoole process.
  • Proxy /api/* to the Winter Boot port (8080 in these guides) with changeOrigin-equivalent behaviour, preserving cookies and paths.
  • Keep Winter Boot bound to localhost or an internal interface; only the proxy faces the internet.
  • Secrets stay server-side: the bundle may hold the /api path prefix, never API keys, DB passwords, or signing keys — those live in backend property sources as the guides show.
  • Sessions vs tokens. The guides use cookie sessions (SID), which fit same-origin browser apps with zero frontend token code. If a mobile app or a third party also calls the API, add token auth (e.g. an Authorization header the interceptor reads via getFirstHeader()) alongside sessions — don’t bend cookies to fit non-browsers.
  • Version the API. The frontend bundle and the backend deploy separately; prefix routes (/api/v1/orders) so an old bundle keeps working while the new one rolls out.
  • Loading and empty states. Every api.* call is async: skeleton rows while pending, an empty-list message (not a blank page), and the notice pattern above for failures.
  • Don’t leak internals. Backend error bodies must stay generic (Forbidden, never stack traces or SQL) — the frontend can only render what the API sends.
  • RBAC — login, sessions, and the route guards this UI calls.
  • ABAC — contextual per-operation policies behind the same endpoints.
  • REST controllers — shaping the JSON contracts the frontend consumes.