Fullstack CourseLearn by building
Back to week 1

Topic

SPA routing: React Router & route guards

Definition

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.

After this you can

  • Explain SPA navigation vs full document reload
  • 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.

Minimal React Router setup

import { BrowserRouter, Routes, Route, Link, useNavigate, useParams } from "react-router-dom";

function App() {
  return (
    <BrowserRouter>
      <nav>
        <Link to="/">Home</Link>
        <Link to="/tickets">Tickets</Link>
      </nav>
      <Routes>
        <Route path="/" element={<Home />} />
        <Route path="/tickets" element={<TicketList />} />
        <Route path="/tickets/:id" element={<TicketDetail />} />
        <Route path="*" element={<NotFound />} />
      </Routes>
    </BrowserRouter>
  );
}

function TicketDetail() {
  const { id } = useParams();
  return <h1>Ticket {id}</h1>;
}

function AfterCreate() {
  const navigate = useNavigate();
  async function onSubmit(e) {
    e.preventDefault();
    const ticket = await createTicket(data);
    navigate(`/tickets/${ticket.id}`);
  }
  return <form onSubmit={onSubmit}>…</form>;
}

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.

Checking your session…

12 questions · concept 4 · syntax 3 · practical 3 · logic 2

Concept1. What normally distinguishes SPA client navigation from full document navigation?
Concept2. In a Vite/CRA SPA, which library typically owns client-side routing?
Syntax3. Which setup wraps a React SPA once so Routes can read browser history?
Syntax4. How do you declare a dynamic ticket detail path with react-router-dom?
Practical5. After a successful create, how should a SPA open the new ticket page?
Practical6. What should a private (protected) route do when the user is not authenticated?
Logic7. Why is hiding an Admin link in React not enough security?
Logic8. Which route is typically public?
Concept9. In this monorepo’s apps/web product shell, what owns routing?
Syntax10. A search form updates the SPA URL without a document reload. What comes first in onSubmit?
Practical11. How do nested React Router layouts usually render child routes?
Concept12. What is a strong candidate for URL state?