My App.jsx contains zero markup. No layout, no styling, no UI logic — just a route tree. That is deliberate. Every starter kit I've inherited buried auth checks and nav state inside components, so debugging a "why can I see this page" question meant grep-hunting through three files. I moved all of it into the route tree, where one file answers it.
<BrowserRouter basename={basename}>
<Routes>
<Route path="/login" element={<Login />} />
<Route path="/register" element={<Register />} />
<Route
element={
<ProtectedRoute>
<Layout />
</ProtectedRoute>
}
>
<Route index element={<Dashboard />} />
<Route
path="/users"
element={
<AdminRoute>
<Users />
</AdminRoute>
}
/>
</Route>
<Route path="/home" element={<Navigate to="/" replace />} />
<Route path="/404" element={<NotFound />} />
<Route path="*" element={<NotFound />} />
</Routes>
</BrowserRouter>
The trick is the parent route with an element but no path: ProtectedRoute wraps Layout, and both Dashboard and Users nest inside. Any page that needs a login lives in that subtree, which means the auth gate is applied structurally — there's no per-page if (!user) redirect to forget. The Users page gets one more layer, AdminRoute, for the role check. Login and Register stay outside the subtree entirely; they're public by omission, which is the kind of default I trust more than a default that needs an exception.
Two small details I kept because they save real headaches:
basename. The router basename comes from Vite's BASE_URL (the base build option), so the app works at a domain root and under a sub-path like a GitHub Pages demo link. Without this, the client-side router and the deployed base URL disagree and every deep link breaks.
The /home redirect. Clients always ask for a /home route that nobody built. A one-line <Navigate to="/" replace /> settles it forever — a redirect entry costs nothing and removes a whole category of "page not found" support questions.
Everything else — the sidebar, the header, the logout button — belongs to Layout, which is just another component rendered through the route tree. App.jsx stays a map; components stay components. When someone asks "who can see /users?", the answer is one nesting level in one file.
This structure ships in my Laravel + React SaaS Starter Kit ($19 one-time) — the full route tree above plus ProtectedRoute and AdminRoute, ready to extend with your own pages.
Top comments (0)