Skip to Content
Grab N GoNext.js Routing

Next.js Routing

Targeting Next.js 16 (App Router)

Syntax

File-Based Routes
app/ ├── page.tsx → / ├── about/page.tsx → /about ├── blog/page.tsx → /blog ├── blog/[slug]/page.tsx → /blog/:slug └── docs/[...slug]/page.tsx → /docs/*
Page Component
// app/about/page.tsx export default function AboutPage() { return <h1>About</h1> } // pages must be default exports
Layout
// app/layout.tsx — wraps all pages export default function RootLayout({ children, }: { children: React.ReactNode }) { return ( <html lang="en"> <body>{children}</body> </html> ) }
Nested Layout
// app/dashboard/layout.tsx // only wraps /dashboard/* routes export default function DashboardLayout({ children, }: { children: React.ReactNode }) { return ( <div className="dashboard"> <Sidebar /> <main>{children}</main> </div> ) }
Loading UI
// app/dashboard/loading.tsx // shown while page.tsx streams in export default function Loading() { return <p>Loading...</p> } // wraps page in Suspense automatically
Error Handling
// app/dashboard/error.tsx 'use client' export default function Error({ error, reset, }: { error: Error reset: () => void }) { return ( <div> <p>{error.message}</p> <button onClick={reset}>Retry</button> </div> ) }
Not Found
// app/not-found.tsx — custom 404 export default function NotFound() { return <h1>Page not found</h1> } // trigger from any server component import { notFound } from 'next/navigation' if (!data) notFound()
Route Groups
app/ ├── (marketing)/ │ ├── layout.tsx ← shared marketing layout │ ├── page.tsx → / │ └── about/page.tsx → /about ├── (app)/ │ ├── layout.tsx ← shared app layout │ └── dashboard/page.tsx → /dashboard

Beginner Patterns

Dynamic Params
// app/blog/[slug]/page.tsx interface Props { params: Promise<{ slug: string }> } export default async function Post({ params }: Props) { const { slug } = await params return <h1>{slug}</h1> } // /blog/hello-world → slug = 'hello-world'
Catch-All Params
// app/docs/[...slug]/page.tsx interface Props { params: Promise<{ slug: string[] }> } export default async function Docs({ params }: Props) { const { slug } = await params return <p>{slug.join(' / ')}</p> } // /docs/a/b/c → slug = ['a', 'b', 'c']
Link Component
import Link from 'next/link' <Link href="/about">About</Link> // dynamic route <Link href={`/blog/${post.slug}`}> {post.title} </Link> // prefetches by default when visible in viewport
useRouter
'use client' import { useRouter } from 'next/navigation' function LogoutButton() { const router = useRouter() return ( <button onClick={() => { logout() router.push('/login') // navigate router.replace('/login') // no back button router.refresh() // re-fetch server components }}> Log out </button> ) }
usePathname
'use client' import { usePathname } from 'next/navigation' function Nav() { const pathname = usePathname() // '/dashboard' return ( <nav> <Link href="/dashboard" className={pathname === '/dashboard' ? 'active' : ''} > Dashboard </Link> </nav> ) }
Search Params
// server component — params from props interface Props { searchParams: Promise<{ q?: string }> } export default async function Search({ searchParams }: Props) { const { q } = await searchParams return <p>Searching: {q}</p> } // /search?q=hello → q = 'hello'

Intermediate Patterns

useSearchParams
'use client' import { useSearchParams } from 'next/navigation' function Filters() { const searchParams = useSearchParams() const sort = searchParams.get('sort') // 'price' return <p>Sorting by {sort}</p> } // /products?sort=price
Update Search Params
'use client' import { useRouter, useSearchParams, usePathname } from 'next/navigation' function SortSelect() { const router = useRouter() const pathname = usePathname() const searchParams = useSearchParams() function setSort(value: string) { const params = new URLSearchParams(searchParams) params.set('sort', value) router.push(`${pathname}?${params}`) } return <select onChange={(e) => setSort(e.target.value)}> <option value="price">Price</option> <option value="name">Name</option> </select> }
Parallel Routes
app/dashboard/ ├── layout.tsx ├── page.tsx ├── @analytics/page.tsx └── @team/page.tsx
// app/dashboard/layout.tsx export default function Layout({ children, analytics, team, }: { children: React.ReactNode analytics: React.ReactNode team: React.ReactNode }) { return <>{children}{analytics}{team}</> }
Intercepting Routes
app/ ├── feed/page.tsx ├── photo/[id]/page.tsx ← full page └── feed/@modal/(.)photo/[id]/page.tsx ← modal overlay
Metadata
// static export const metadata = { title: 'My App', description: 'A cool app', } // dynamic export async function generateMetadata( { params }: Props ) { const { slug } = await params const post = await getPost(slug) return { title: post.title } }
Proxy (was Middleware)
// proxy.ts (project root — renamed from middleware.ts) import { NextResponse } from 'next/server' export function proxy(req: Request) { const cookie = req.headers.get('cookie') ?? '' if (!cookie.includes('token=')) { return NextResponse.redirect(new URL('/login', req.url)) } return NextResponse.next() } export const config = { matcher: '/dashboard/:path*', } // runtime is always nodejs — edge is NOT supported in proxy

Advanced Patterns

generateStaticParams
// app/blog/[slug]/page.tsx // pre-render these at build time export async function generateStaticParams() { const posts = await getPosts() return posts.map((p) => ({ slug: p.slug })) } // generates /blog/first, /blog/second, etc.
Route Handlers
// app/api/users/route.ts import { NextResponse } from 'next/server' export async function GET() { const users = await db.user.findMany() return NextResponse.json(users) } export async function POST(req: Request) { const body = await req.json() const user = await db.user.create({ data: body }) return NextResponse.json(user, { status: 201 }) }
Dynamic Route Handler
// app/api/users/[id]/route.ts interface Ctx { params: Promise<{ id: string }> } export async function GET(_req: Request, ctx: Ctx) { const { id } = await ctx.params const user = await db.user.findUnique({ where: { id }, }) if (!user) { return NextResponse.json( { error: 'Not found' }, { status: 404 } ) } return NextResponse.json(user) }
Redirect
import { redirect, permanentRedirect } from 'next/navigation' // in a server component or action async function checkAuth() { const user = await getUser() if (!user) redirect('/login') // 307 if (user.banned) permanentRedirect('/banned') // 308 } // in next.config // redirects: async () => [ // { source: '/old', destination: '/new', permanent: true }, // ]
Rewrite & Headers
// next.config.ts const config = { async rewrites() { return [ { source: '/api/:path*', destination: 'https://api.example.com/:path*' }, ] }, async headers() { return [{ source: '/api/:path*', headers: [ { key: 'Access-Control-Allow-Origin', value: '*' }, ], }] }, }
Route Segment Config
// app/blog/page.tsx // opt into dynamic rendering export const dynamic = 'force-dynamic' // or force static export const dynamic = 'force-static' // revalidate every 60 seconds export const revalidate = 60 // choose runtime export const runtime = 'edge' // or 'nodejs'
Last updated on