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.
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.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}} /> refreshREDIRECT 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
- Declare routes as data, not scattered conditionals β a route table is inspectable, testable, and reorderable.
- Guard subtrees, not individual pages, so new nested routes inherit protection instead of forgetting it.
- Handle the loading state. Until the session check resolves, render a splash β never assume "not yet loaded" means "anonymous".
- Preserve the intended destination and return the user there after login; reject absolute URLs in
nextto avoid open redirects. - Code-split at the route level (5.7) β this is where lazy loading pays the most.
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
| Decision | Consequence |
|---|---|
| Guard in the route tree | Central, inherited, testable |
| Guard inside each page | Easy to forget on a new page |
replace on redirect | Back button doesn't loop |
| Role checks client-side | UX 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
replaceso 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.