Skip to content
Back to Build Tutorials

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 page

Example

// 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.