A runnable Next.js App Router app that demonstrates the SSR RPC bridge: how a
client component can call api.module.method() with full type-safety while
the backend URL, paths, and OpenAPI document never reach the browser. Backed
by the free DummyJSON API — no backend setup needed.
VIEW live example at https://api-client-nextjs.vercel.app/ 📖 Feature documentation:
../../docs/ssr-rpc-bridge.mdand../../docs/rpc-rate-limiting.md. This README is the guided tour of this app.Note: this example uses a modified Next.js. Read
node_modules/next/dist/docs/before writing Next-specific code (seeAGENTS.md).
# from the monorepo root
pnpm install
pnpm --filter @developerehsan/api-client build # build the library once
cd examples/nextjs
pnpm devOpen http://localhost:3000 and keep the Network tab
open — the whole point is that client-component calls show only a same-origin
POST carrying { module, method, args }, never dummyjson.com or any path.
| Route | File | What it shows |
|---|---|---|
/ |
app/ProductDemo.tsx |
The bridge from a client component via a Next.js Server Action |
/http |
app/http/HttpDemo.tsx |
The same bridge via the framework-agnostic POST /api/rpc route |
/server |
app/server/page.tsx |
Direct server-side usage (RSC) — no bridge needed, nothing leaks |
Browser (client component) Server (holds all secrets)
───────────────────────── ─────────────────────────────────────
api.products.getProductById({ id }) lib/api/api.config.ts
│ createRpcClient<Api>(transport) ├─ createTypedClient(...) ← real client
│ Api = typeof serverApi (type-only) └─ createRpcHandler(api, { expose, … })
▼ POST { module, method, args } ▲ allowlist → rate-limit → authorize
transport ───────────────────────────────────┘ → dispatch → transformResult
| Piece | File |
|---|---|
Real client + createRpcHandler (the trust boundary) |
lib/api/api.config.ts |
| Server Action transport | app/actions.ts |
| Generic HTTP route transport | app/api/rpc/route.ts |
| Browser bridge client (Server Action) | lib/api/rpc-client.ts |
| Browser bridge client (generic HTTP) | lib/api/rpc-http-client.ts |
TanStack Query over the bridge (paths-stripped rpcModules) |
lib/api/query.ts |
Each maps to a feature in the docs:
| # | Scenario | Demonstrates |
|---|---|---|
| 1 | Direct read (getProductById) |
Typed call over the bridge; images redacted by transformResult |
| 2 | Direct write + authz (addProduct) |
The authorize hook gates writes behind an editor cookie |
| 3 | Cancellation | An AbortSignal cancels the call locally (not sent over the wire) |
| 4 | Uniform denial (deleteProduct) |
A non-exposed method returns the same generic error as an unknown one (no enumeration) |
| 5 | Batching | 3 calls in one tick → one POST; each sub-call validated individually server-side |
| 6 | TanStack Query | useQuery through the bridge with the paths-stripped descriptor |
| 7 | TanStack Mutation | useMutation + cache invalidation |
The handler in lib/api/api.config.ts wires every guard:
expose— deny-by-default allowlist; module + method names are typed againstApi, so a typo is a compile error.onRequest— acreateRateLimiter(30 calls / 10s per session cookie) that rejects over-budget calls.authorize— reads open; writes (addProduct/updateProduct/deleteProduct) require ademo_editorcookie. A denial looks identical to an unknown method.transformResult— strips a product's bulkyimagesarray before it crosses the wire (least-privilege responses).onError— logs the full error server-side; only{ name, status, code, message }ever reaches the browser.maxBatchSize— bounds RPC batching.
See the full guarantee table in
../../docs/ssr-rpc-bridge.md.
- On
/, click Fetch product — note the response has noimagesfield. - Click Try forbidden (deleteProduct) — denied (not exposed).
- Toggle Become editor, then Add a product — now allowed.
- Click Fetch #1, #2, #3 together and watch the Network tab: one POST.
- Visit
/httpfor thehttpTransportvariant and/serverfor direct RSC usage.
The spec lives at lib/api/openapi.json:
npx @developerehsan/api-client generate \
--input ./lib/api/openapi.json --output ./lib/api/types/generatedThis also emits api.rpc.ts — the paths-stripped descriptor the browser
bridge + TanStack integration use so no backend path ships to the client.