Routing Setup
Configure file-based routing with layouts, nested routes, and typed params.
Why
Routing is the skeleton of the app — every page, every layout, every navigation transition flows through it. A well-configured router gives you nested layouts (sidebar + content), typed route params (no more string parsing), and code-splitting per route (faster initial load). File-based routing (Next.js app router, TanStack Router) eliminates the route config file — the file system IS the route tree, so adding a page is creating a file, not editing a config. Typed params (useParams<{ id: string }>()) catch a wrong param name at compile time instead of rendering undefined at runtime.
When
When the app has ≥3 pages. Takes 1-2 h. Skip for single-page apps.
How
1. Use the framework's file-based router (Next.js app/, TanStack Router, Remix)
- app/dashboard/users/[id]/page.tsx → /dashboard/users/:id
- Layouts: layout.tsx wraps all pages in the directory
2. Type route params with the framework's typed params
- Next.js: const { id } = useParams<{ id: string }>()
- TanStack Router: generate a routeTree.gen.ts with full type safety
3. Add a root layout with the shared chrome (header, footer, error boundary)
- This layout wraps every route; keep it minimal (no data fetching)
- Nested layouts compose: root > dashboard > users > [id]
4. Add a loading.tsx (Next.js) or suspense fallback for each route
- This shows a skeleton during data fetching, not a blank page
- Code-split per route so the initial bundle only includes the landing page
5. Add a not-found.tsx and error.tsx for each route segment
- 404s and errors should render in the app chrome, not a white pageExample
// app/dashboard/users/[id]/page.tsx (Next.js app router)
import { useParams } from "next/navigation";
export default async function UserPage({ params }: { params: { id: string } }) {
const user = await getUser(params.id); // typed params, no parsing
return <UserDetail user={user} />;
}
// app/dashboard/layout.tsx (nested layout)
export default function DashboardLayout({ children }: { children: React.ReactNode }) {
return (
<div className="flex">
<Sidebar />
<main className="flex-1">{children}</main>
</div>
);
}
// app/dashboard/users/[id]/loading.tsx
export default function Loading() { return <UserDetailSkeleton />; }
// app/dashboard/users/[id]/error.tsx
export default function Error({ error }: { error: Error }) {
return <ErrorBoundary error={error} />;
}Common Problems
- ⚠Parsing params with parseInt without validation, so /users/abc renders NaN as an ID.
- ⚠Putting data fetching in the root layout, so every navigation re-fetches everything.
- ⚠No loading state, so navigating to a data-heavy page shows a blank screen for 2 seconds.
- ⚠A single error boundary at the root, so a bug in one route kills the entire app.
Best Practices
- ✓Use typed route params (Next.js: params is typed; TanStack Router: full path type safety) to catch typos at compile time.
- ✓Keep layouts thin — shared chrome only, no data fetching — so they don't block navigation.
- ✓Add a loading.tsx at every data-fetching route so users see a skeleton, not a blank page.
- ✓Use parallel routes (Next.js @slot) for modals and conditionals that share a layout.