FSD Frontend System Design

Part V β€” The Craft Β· Lesson 10.4

Routing & Protected Routes

In one lineA route table plus a guard that knows three answers: yes, no, and not yet.

Where you've seen itHitting refresh on /orders/42, seeing the login screen, and being returned to /orders/42 after signing in.

  • Time16 min
  • DiagramRoute tree with guard nodes + the three-state auth check
  • Chapter10 Β· Low Level Design

Diagram

diagram
ROUTE TREE WITH GUARDS

  /                          public
  β”œβ”€β”€ /login                 public  (redirect away if already in)
  β”œβ”€β”€ /products              public
  β”‚   └── /products/:id      public
  β”œβ”€β”€ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
  β”‚   β”‚ <RequireAuth>                       β”‚  guard node
  β”‚   β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€
  β”‚   β”œβ”€β”€ /orders            authenticated  β”‚
  β”‚   β”œβ”€β”€ /orders/:id        authenticated  β”‚
  β”‚   └── β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚
  β”‚       β”‚ <RequireRole role="admin">    β”‚ β”‚  nested guard
  β”‚       β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€ β”‚
  β”‚       └── /admin/*        admin only   β”‚ β”‚
  β”‚       β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β”‚
  β”‚   β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
  └── *                      404

  Guards wrap SUBTREES, so a new child route is protected by default.
diagram
THE THREE-STATE CHECK β€” the bug is always the missing third state

  auth state          render
  ─────────────────────────────────────────────────────
  loading   (unknown) β†’ skeleton / splash    ← FORGETTING THIS
  authed    (yes)     β†’ the page               causes a redirect
  anonymous (no)      β†’ <Navigate to="/login"  flash on every
                          state={{from}} />     refresh
diagram
REDIRECT WITH RETURN PATH

  user hits /orders/42
        β”‚ not authenticated
        β–Ό
  /login?next=/orders/42   (or router state)
        β”‚ successful login
        β–Ό
  navigate(next ?? '/')    ← validate `next` is a RELATIVE path,
                             or it's an open redirect (3.10-adjacent)

How it works

  1. Declare routes as data, not scattered conditionals β€” a route table is inspectable, testable, and reorderable.
  2. Guard subtrees, not individual pages, so new nested routes inherit protection instead of forgetting it.
  3. Handle the loading state. Until the session check resolves, render a splash β€” never assume "not yet loaded" means "anonymous".
  4. Preserve the intended destination and return the user there after login; reject absolute URLs in next to avoid open redirects.
  5. Code-split at the route level (5.7) β€” this is where lazy loading pays the most.
jsx
function RequireAuth({ children }) {
  const { status } = useAuth();               // 'loading' | 'authed' | 'anon'
  const location = useLocation();

  if (status === 'loading') return <Splash />;                  // third state
  if (status === 'anon')
    return <Navigate to="/login" replace state={{ from: location }} />;
  return children;
}

Trade-offs

DecisionConsequence
Guard in the route treeCentral, inherited, testable
Guard inside each pageEasy to forget on a new page
replace on redirectBack button doesn't loop
Role checks client-sideUX only β€” the API still enforces (3.5)

Interview angle

"How do you protect routes in an SPA?"

  • A guard component wrapping the protected subtree, driven by a three-state auth value so refresh doesn't flash the login page.
  • Preserve and validate the return path, and use replace so the back button doesn't bounce between login and page.
  • Say plainly that this is presentation only β€” the server authorises every request regardless of what the client renders.

Recap

  • Routes as data, guards on subtrees, code-split per route.
  • The third state β€” loading β€” is what separates a working guard from a flickering one.
  • Client-side route protection is UX; the API is the actual boundary.