React dashboard architecture: how I organize a real SaaS frontend
I've inherited too many React frontends where fetch calls were scattered across components, tokens were read from localStorage in twelve places, and adding one protected page meant touching five files. When I built the frontend for my SaaS starter kit, I set five structural rules. Here's how they work, with the real code.
1. One API client module — components never touch HTTP directly
Everything goes through a single axios instance in src/api/client.js: base URL from env, a request interceptor attaching the Bearer token from localStorage, and a response interceptor that on 401 clears the token and redirects to /login (skipping /login and /register themselves). Base URL from env, token attached automatically, expired sessions handled globally. When an endpoint changes, you edit one file. Components import this client and call it — they never construct URLs or headers themselves.
2. Auth state lives in one context
A single AuthContext owns the session: user, token, loading state, and login/register/logout functions. On mount it restores the session from the stored token. Logout is best-effort by design — the session is cleared locally no matter what the server says. No component ever reads localStorage directly. If you change how sessions work, there's exactly one place to change it.
3. Route guards, not scattered checks
Protected pages are wrapped, not conditionally rendered inside page components: a ProtectedRoute that shows a spinner while loading and redirects to /login without a session, plus an AdminRoute adding a role check. The loading state matters more than people think — without it, the app flashes the login page on every refresh while the session restores.
4. Layout is a component, pages are just content
The dashboard shell — sidebar, topbar, content area — is a single Layout component. New pages are just content dropped into it. Adding a settings page means writing the settings content, not rebuilding navigation for the tenth time.
5. Demo mode that doesn't pollute the real code
The kit ships with a clickable demo that runs without a backend. The trick is a seam in exactly one place: const client = isDemoMode() ? createDemoClient() : axios.create({ ... }). A demoMock.js implements the same interface against in-memory data. The rest of the app can't tell the difference. One conditional at the client boundary, zero demo hacks scattered through the UI.
Boring on purpose. Every file has one job, and a new developer can map the whole frontend in an afternoon.
I packaged this frontend together with its Laravel API backend — Sanctum auth, roles, REST conventions, seeders — into a starter kit, because rebuilding this setup on every project was costing me a week each time:
Laravel + React SaaS Starter Kit — $19, one-time: https://kamranofficial.gumroad.com/l/mhekoig
What would you organize differently? Genuinely curious — drop a comment.
Top comments (1)
Some comments may only be visible to logged-in visitors. Sign in to view all comments.