The YouTeacher web app is a Next.js frontend that does not talk to a single monolith. It fans out to a fleet of backend microservices — auth, profile, jobs, talent search, content, admin — each behind its own path prefix. Four mechanisms keep that fan-out uniform, so that feature code can ask for data without re-solving networking, retries, auth, or "am I on the server or in the browser?" every time.
One HTTP client, all requests through it
Every backend call routes through a single client built by createHttpClient (with a lazy singleton via getHttpClient). Centralizing it means the cross-cutting concerns live in one place:
- Transient-failure retries. A
502,503, or504is retried with exponential backoff (base delay doubling per attempt, three attempts by default). Network errors that aren't outright cancellations are retried the same way. - Rate limiting is not retried. A
429throws a typedRateLimitErrorthat carries the server's retry-after hint, and fires anonRateLimitcallback once — the UI surfaces "too many requests" rather than hammering the service. - Session expiry is global. A
401triggers anonUnauthorizedcallback so the auth layer can clear session state and redirect, no matter which call hit the expiry. - Aborts are distinguished from failures. A request cancelled by navigation unmount (an
AbortError, or a "Failed to fetch" that really means "the page went away") is rethrown untouched, never retried and never logged as an error. - Typed errors.
HttpErrorcarries status and an optional machine code; theRateLimitErrortype guard is written to survive prototype-chain breakage after bundling.
Auth, at the mechanism level
The client attaches two layers of authentication to outbound calls, and the design keeps them separate on purpose:
- A session answers who is making the request — user identity — and rides along automatically with credentialed requests.
- A short-lived first-party service token answers where the request comes from — it proves the call originates from a trusted YouTeacher service rather than an external caller who guessed an internal URL. On the server the token is minted in-process; in the browser it is obtained through a server action. The client adds it to every request.
The point of splitting them is that user identity never has to be embedded in the service token — the token is about trust between services, the session is about the person.
Per-service URL resolution
Each backend has its own configurable base URL, read from environment config, with a deliberate split:
- Server vs. client variants. Server-side code resolves URLs dynamically at runtime and memoizes them; browser code must reference build-time literals, because Next.js inlines the public config values at build time and cannot read them dynamically later.
- Fallback chains. A specialized base (say, a dedicated search host) falls back to the general service base when it isn't set, so most deployments configure one URL and get the rest for free.
- Override wins, else base + path. A resolver takes an optional full-URL override; absent that, it joins a normalized base with a normalized path. Placeholder builders fill
:id,:token, and:providersegments.
The "am I in the browser?" split
Because server and browser resolve URLs from different sources, each service API function branches on typeof window. The jobs API is the model: in the browser it reads the inlined public constants; on the server it calls the cached getter functions. The server path also keeps a small LRU cache for repeated searches. Missing results map to null (a 404 on a detail fetch), and any other non-OK response is turned into a typed error rather than a raw throw.
The gateway
In front of all of this sits a single reverse proxy that routes by URL path prefix — one prefix per service — and forwards headers and cookies through unchanged. The browser sees a single origin; the request quietly fans out to whichever service owns that prefix. This is what lets the frontend treat "the backend" as one address while it is really many.
Viewer strategy
A recurring shape in this product is "the same action, but the viewer might be an employer or a recruiter." Rather than branch on viewer type at every call site, a strategy resolves it once: fetchViewerContext fetches both the employer and recruiter profiles in parallel and decides the viewer is authorized (a verified employer, or an approved recruiter) or unauthorized with a specific reason. createViewerStrategy then returns an adapter — employer or recruiter — that exposes the same fetchQuota / fetchUnlockedTalents interface. Feature code holds a strategy and never asks "which kind of viewer is this?" again.
Why it reads as one system
None of these pieces is exotic on its own — a retrying fetch wrapper, an env-driven URL table, a reverse proxy, a strategy object. What makes the integration coherent is that they compose: the URL table feeds the isBrowser split, the isBrowser split feeds the one client, the one client carries auth and retries, and the gateway makes it all look like one origin. A new backend endpoint is added by naming its base and path in config and writing one branched fetch function — the retries, the auth token, the error typing, and the routing come along automatically.