System Designmedium
Implement role-based route guards on the client
Design a React route-guard pattern for an admin console with multiple roles (admin, analyst, viewer). Cover the security caveats — what the client guard actually buys you and what it does not.
Asked at CrowdStrike, Stripe
Answer
// First, the honest part: // Client-side guards are a UX layer, NOT a security boundary. // Every protected action MUST be re-checked on the server. // The client guard exists to: // 1) Hide controls a user cannot use (less confusing). // 2) Avoid wasted requests that will 403 anyway. // 3) Redirect unauth'd users to login before they hit a broken UI. import { Navigate, Outlet, useLocation } from 'react-router-dom' type Role = 'admin' | 'analyst' | 'viewer' interface Session { userId: string roles: Role[] } const SessionCtx = createContext<Session | null>(null) export const useSession = () => useContext(SessionCtx) export function hasAny(session: Session | null, allowed: Role[]): boolean { if (!session) return false return session.roles.some((r) => allowed.includes(r)) } export function RequireRole({ allowed }: { allowed: Role[] }) { const session = useSession() const location = useLocation() if (!session) { return <Navigate to="/login" replace state={{ from: location }} /> } if (!hasAny(session, allowed)) { return <Navigate to="/403" replace /> } return <Outlet /> } // Usage with React Router <Routes> <Route element={<RequireRole allowed={['admin', 'analyst', 'viewer']} />}> <Route path="/dashboard" element={<Dashboard />} /> </Route> <Route element={<RequireRole allowed={['admin', 'analyst']} />}> <Route path="/incidents" element={<Incidents />} /> </Route> <Route element={<RequireRole allowed={['admin']} />}> <Route path="/admin/users" element={<UserAdmin />} /> </Route> </Routes> // Per-control gating function DangerButton() { const session = useSession() if (!hasAny(session, ['admin'])) return null return <button onClick={deleteEverything}>Delete</button> } // Pitfalls to mention in the interview: // - Don't ship admin-only code splits to non-admins (use route-level lazy loading // so the JS bundle isn't even fetched). // - Refresh the session on focus/visibility — roles can change. // - Treat `session` as untrusted input; never let it grant access to data the // server wouldn't return anyway. // - For deep-link sharing, distinguish "not logged in" vs "logged in but forbidden" // so users get the right next action.