
Next.js 15/16 Routing Guide 2026
Learn modern routing in Next.js 15 and 16 with App Router, dynamic routes, and essential routing patterns for 2026.
Next.js 15/16 Routing Guide
Next.js App Router brings a powerful file-system-based routing system built on React Server Components and React 19. This guide covers the essential routing patterns in Next.js 15/16.
What's New in Next.js 15/16
Key Features:
- Turbopack (Stable) - Lightning-fast development bundler
- Async Request APIs -
paramsandsearchParamsnow async - Cache Components - New
use cachedirective - React 19 - Full support with Server Components
- Enhanced Routing - Optimized navigation and prefetching
App Directory Structure
The app directory is the foundation of Next.js routing. Every folder becomes a route, and special files define behavior:
app/
├── layout.tsx # Root layout (required)
├── page.tsx # Homepage route
├── loading.tsx # Loading UI
├── error.tsx # Error handling
└── not-found.tsx # 404 pageFile Hierarchy: layout → error → loading → page
Basic Routing
Every folder with a page.tsx becomes a route.
Example: app/about/page.tsx
export default function AboutPage() {
return <h1>About Us</h1>;
}URL: /about
Folder Structure
app/
├── page.tsx # /
├── about/
│ └── page.tsx # /about
└── blog/
└── page.tsx # /blogDynamic Routes
Use brackets [param] for dynamic URL segments.
File: app/products/[id]/page.tsx
interface PageProps {
params: Promise<{ id: string }>;
}
export default async function ProductPage({ params }: PageProps) {
const { id } = await params;
return <h1>Product {id}</h1>;
}URLs: /products/1, /products/laptop, etc.
Important: In Next.js 15+,
paramsandsearchParamsare async!
Catch-All Routes
Match multiple segments with [...slug]:
File: app/docs/[...slug]/page.tsx
interface PageProps {
params: Promise<{ slug: string[] }>;
}
export default async function DocsPage({ params }: PageProps) {
const { slug } = await params;
return <p>Path: /{slug.join('/')}</p>;
}Matches: /docs/api/reference → slug = ['api', 'reference']
Optional Catch-All [[...slug]]
Use double brackets to also match the parent route:
File: app/docs/[[...slug]]/page.tsx
Matches: /docs AND /docs/api/reference
Route Groups
Organize routes without affecting URLs using (folder):
app/
├── (marketing)/
│ ├── page.tsx # /
│ └── about/
│ └── page.tsx # /about
└── (dashboard)/
└── dashboard/
└── page.tsx # /dashboardEach group can have its own layout.tsx with different styles/navigation.
Parallel Routes
Render multiple pages in the same layout using @folder slots:
app/dashboard/
├── layout.tsx
├── @team/page.tsx
└── @analytics/page.tsx// layout.tsx
export default function Layout({
children,
team,
analytics,
}: {
children: React.ReactNode;
team: React.ReactNode;
analytics: React.ReactNode;
}) {
return (
<>
{children}
<aside>
{team}
{analytics}
</aside>
</>
);
}Intercepting Routes
Show modals without changing the URL using route interception:
(.)- Same level(..)- One level up(...)- From root
Example: Photo modal
app/photos/
├── page.tsx # Grid
├── [id]/page.tsx # Full page
└── (..)photos/[id]/page.tsx # ModalClick from grid → shows modal. Refresh → shows full page.
Layouts and Error Handling
Root Layout (Required)
// app/layout.tsx
export default function RootLayout({
children,
}: {
children: React.ReactNode;
}) {
return (
<html lang="en">
<body>{children}</body>
</html>
);
}Loading States
// app/dashboard/loading.tsx
export default function Loading() {
return <div>Loading...</div>;
}Error Boundaries
// app/dashboard/error.tsx
'use client';
export default function Error({
error,
reset,
}: {
error: Error;
reset: () => void;
}) {
return (
<div>
<h2>Something went wrong!</h2>
<button onClick={reset}>Try again</button>
</div>
);
}Not Found
// app/not-found.tsx
export default function NotFound() {
return <h2>Page Not Found</h2>;
}Navigation
Link Component
import Link from 'next/link';
<Link href="/about">About</Link>
<Link href="/blog/post-1">First Post</Link>
<Link href="/products?sort=price">Products</Link>Programmatic Navigation
'use client';
import { useRouter } from 'next/navigation';
export default function LoginButton() {
const router = useRouter();
return <button onClick={() => router.push('/dashboard')}>Login</button>;
}Hooks
'use client';
import { usePathname, useSearchParams } from 'next/navigation';
const pathname = usePathname(); // /blog/post-1
const searchParams = useSearchParams(); // ?sort=price
const sort = searchParams.get('sort'); // 'price'Metadata
Static Metadata
import { Metadata } from 'next';
export const metadata: Metadata = {
title: 'Blog Posts',
description: 'Read our latest articles',
};Dynamic Metadata
export async function generateMetadata({
params
}: {
params: Promise<{ slug: string }>
}): Promise<Metadata> {
const { slug } = await params;
const post = await fetchPost(slug);
return {
title: post.title,
description: post.excerpt,
};
}API Routes
Create API endpoints with route.ts:
// app/api/posts/route.ts
import { NextResponse } from 'next/server';
export async function GET() {
const posts = await fetchPosts();
return NextResponse.json(posts);
}
export async function POST(request: Request) {
const body = await request.json();
const post = await createPost(body);
return NextResponse.json(post, { status: 201 });
}Dynamic API Routes
// app/api/posts/[id]/route.ts
export async function GET(
request: Request,
{ params }: { params: Promise<{ id: string }> }
) {
const { id } = await params;
const post = await fetchPost(id);
return NextResponse.json(post);
}Static Generation
Pre-render dynamic routes at build time:
// app/blog/[slug]/page.tsx
export async function generateStaticParams() {
const posts = await fetchAllPosts();
return posts.map((post) => ({
slug: post.slug,
}));
}
export default async function PostPage({
params
}: {
params: Promise<{ slug: string }>
}) {
const { slug } = await params;
const post = await fetchPost(slug);
return <article>{post.content}</article>;
}Middleware
Run code before requests complete:
// middleware.ts
import { NextResponse } from 'next/server';
import type { NextRequest } from 'next/server';
export function middleware(request: NextRequest) {
const token = request.cookies.get('token');
if (!token && request.nextUrl.pathname.startsWith('/dashboard')) {
return NextResponse.redirect(new URL('/login', request.url));
}
return NextResponse.next();
}
export const config = {
matcher: ['/dashboard/:path*'],
};Best Practices
- Use Server Components by default - Only add
'use client'when needed - Async params - Always await
paramsandsearchParamsin Next.js 15+ - Loading states - Add
loading.tsxfor better UX - Error boundaries - Use
error.tsxto handle errors gracefully - Route groups - Organize routes with
(folder)without affecting URLs - Static generation - Use
generateStaticParamsfor pre-rendering - Metadata - Always add proper SEO metadata
Key Takeaways
- File-system routing - Folders create routes,
page.tsxmakes them public - Dynamic routes - Use
[param]for dynamic segments - Catch-all -
[...slug]for multi-segment paths - Async params - Required in Next.js 15+
- Server Components - Default and recommended
- Turbopack - Fast development with stable bundler


