Client-side routing maps URL paths to React UI without a full document reload. In a Vite/CRA SPA, react-router-dom owns that mapping; public routes render for everyone while private (protected) routes require an auth check before rendering the destination.
In simpler words
A router watches the address bar and picks which screen to show. Some screens are open; some require login first.
Learn how React apps route with react-router-dom (BrowserRouter, Routes, Link, navigate, params), how public vs private routes work, then how this monorepo uses Next App Router instead for the product shell.
Wire BrowserRouter, Routes, Route, Link, and useNavigate
Read path params and search params
Implement a private-route guard that redirects unauthenticated users
Name when this monorepo uses Next Link instead of React Router
Full reload vs client transition
A normal <a href> asks the browser for a new HTML document. React state, Redux stores, and Query caches restart.
A client router updates the URL and swaps the matched component tree while the JS runtime stays alive — drafts and caches can survive when that is intentional.
Same destination, different cost
// Full document navigation
<a href="/tickets">Tickets</a>
// SPA navigation (react-router-dom)
import { Link } from "react-router-dom";
<Link to="/tickets">Tickets</Link>
Both change the URL; only the SPA Link keeps the app mounted.
Mistake: treating every internal click like an external site
// Wrong inside an SPA shell
<a href="/dashboard">Dashboard</a>
// Right
<Link to="/dashboard">Dashboard</Link>
Use <a> for external sites; use the router Link for in-app paths.
How React routes with react-router-dom
Install react-router-dom in a SPA (Vite/CRA). Wrap the app once in BrowserRouter (or createBrowserRouter + RouterProvider in data APIs).
Declare a route tree with Routes and Route. Declarative Link navigates on click. useNavigate() navigates after an event (login success, form submit). useParams / useSearchParams read URL state.
Nested routes use an Outlet so a layout can wrap child pages — the same idea as Next layouts, different API.
BrowserRouter owns history; Routes pick the element; Link/navigate change location.
Mistake: navigate during render
// Wrong
function Page() {
const navigate = useNavigate();
navigate("/tickets"); // side effect in render
return null;
}
// Right — after an event or completed async work
<button type="button" onClick={() => navigate("/tickets")}>Open</button>
Navigation is a side effect. Call it from handlers or post-mutation success, not while rendering.
Public vs private routes
A public route renders for everyone: login, marketing, password reset, health status.
A private (protected) route requires proof of auth before showing the page. The usual pattern is a wrapper component that reads session state and either renders <Outlet /> / children or redirects to /login with a return URL.
Auth truth still comes from the server (Nest cookie/session). The guard only controls what the client UI shows; Nest must still reject unauthorized API calls.
ProtectedRoute with redirect
import { Navigate, Outlet, useLocation } from "react-router-dom";
function ProtectedRoute({ isAuthenticated }: { isAuthenticated: boolean }) {
const location = useLocation();
if (!isAuthenticated) {
return <Navigate to="/login" replace state={{ from: location }} />;
}
return <Outlet />;
}
// Route tree
<Routes>
<Route path="/login" element={<LoginPage />} /> {/* public */}
<Route element={<ProtectedRoute isAuthenticated={!!user} />}>
<Route path="/tickets" element={<TicketList />} /> {/* private */}
<Route path="/pulse" element={<Pulse />} />
</Route>
</Routes>
// After login — send user back
const from = location.state?.from?.pathname ?? "/tickets";
navigate(from, { replace: true });
Layout route wraps private children. Unauthenticated users never mount the private page tree.
Mistake: client-only “security”
// Wrong — hide the page but still call Nest without a session
if (!user) return null;
return <AdminPanel />; // API still callable if URL is known
// Right — guard UI AND rely on Nest 401/403
<ProtectedRoute />
// Nest JwtAuthGuard rejects missing/invalid cookies
Private routes improve UX; Nest authorization is the real security boundary.
Bridge to this monorepo (Next)
Here, Next App Router owns product routing: folders under src/app, Link from next/link, useRouter from next/navigation.
The mental model is the same: declarative links for clicks, imperative navigate after events, layouts for shared chrome, and auth-aware redirects for private areas.
Do not install react-router-dom beside Next in apps/web — one router per app.
Same idea, Next APIs
import Link from "next/link";
import { useRouter } from "next/navigation";
<Link href="/tickets">Tickets</Link>
router.push(`/tickets/${id}`);
Week 3 covers Next Link and useRouter in depth.
Live playground
Router & guards sandbox
Toggle auth and path — see public vs private behavior.
Simulated react-router-dom: public /login vs private /course/* (ProtectedRoute).
Requested: /login → rendering /login
<Route element={<ProtectedRoute />}>
<Route path="/course/frontend" element={<FrontendRoadmap />} />
</Route>
// Product app here: Next App Router, not react-router-dom
Keep in mind
Public = anyone; private = auth check then Outlet (or Next redirect).
URL should be bookmarkable for filters and entity ids.
Learn React Router for SPA jobs; use Next routing inside this repo.
preventDefault on SPA forms so the document does not reload.
Test
Check your understanding
At least 10 questions — mix of concept, syntax, practical, and logic. Score ≥ 80% (enforced by the API) to save progress.