Routing
Next.js App Router structure and navigation
Next.js 16 App Router uses file-based routing with powerful features like layouts, loading states, and Cache Components mode.
File-Based Routing
URL Mapping:
app/page.tsx→/app/blog/page.tsx→/blogapp/blog/[slug]/page.tsx→/blog/my-post
Special Files
| File | Purpose |
|---|---|
page.tsx | Route UI |
layout.tsx | Shared wrapper |
loading.tsx | Loading state |
error.tsx | Error boundary |
not-found.tsx | 404 page |
route.ts | API endpoint |
Route Groups
Use (name) to organize without affecting URLs.
app/
├── (auth)/
│ ├── sign-in/page.tsx → /sign-in
│ └── layout.tsx (auth layout)
├── (base)/
│ ├── blog/page.tsx → /blog
│ └── layout.tsx (main layout)
└── layout.tsx (root layout)Route groups like (auth) don't appear in URLs. They're for organization and applying different layouts.
Layouts
Layouts wrap pages and persist across navigation.
// app/layout.tsx (required)
export default function RootLayout({ children }: { children: React.ReactNode }) {
return (
<html lang="en">
<body>{children}</body>
</html>
);
}
// app/(base)/layout.tsx (nested)
export default function BaseLayout({ children }: { children: React.ReactNode }) {
return (
<>
<Navigation />
<main>{children}</main>
<Footer />
</>
);
}Dynamic Routes
Single Segment
// app/blog/[slug]/page.tsx
export default async function BlogPost({ params }: { params: { slug: string } }) {
const post = await getPostBySlug(params.slug);
return <article>{post.title}</article>;
}
// Generate static pages
export async function generateStaticParams() {
const posts = await getAllPosts();
return posts.map((post) => ({ slug: post.slug }));
}Catch-All
// app/docs/[...slug]/page.tsx
export default async function DocsPage({ params }: { params: { slug: string[] } }) {
const page = await getDocPage(params.slug);
return <article>{page.content}</article>;
}Examples:
/docs/getting-started→slug = ["getting-started"]/docs/core/installation→slug = ["core", "installation"]
Optional Catch-All
// app/shop/[[...categories]]/page.tsx
export default function ShopPage({ params }: { params: { categories?: string[] } }) {
const categories = params.categories || [];
return <ProductList categories={categories} />;
}Loading States
// app/dashboard/loading.tsx
export default function Loading() {
return <div className="animate-spin">Loading...</div>;
}Automatically wrapped in Suspense. Shows while page.tsx loads.
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>
<p>{error.message}</p>
<button onClick={reset}>Try again</button>
</div>
);
}Navigation
Link Component
import Link from 'next/link';
<Link href="/blog">Blog</Link>
<Link href="/docs" prefetch={false}>Docs</Link>useRouter Hook
'use client';
import { useRouter } from 'next/navigation';
export function LoginForm() {
const router = useRouter();
const handleSubmit = async () => {
await login();
router.push('/dashboard');
};
}redirect Function
import { redirect } from 'next/navigation';
export default async function ProfilePage() {
const user = await getCurrentUser();
if (!user) redirect('/sign-in');
return <Profile user={user} />;
}API Routes
// app/api/users/route.ts
import { NextRequest, NextResponse } from 'next/server';
export async function GET(req: NextRequest) {
const users = await prisma.user.findMany();
return NextResponse.json(users);
}
export async function POST(req: NextRequest) {
const body = await req.json();
const user = await prisma.user.create({ data: body });
return NextResponse.json(user, { status: 201 });
}Dynamic API Routes
// app/api/users/[id]/route.ts
export async function GET(
req: NextRequest,
{ params }: { params: { id: string } }
) {
const user = await prisma.user.findUnique({ where: { id: params.id } });
if (!user) {
return NextResponse.json({ error: 'Not found' }, { status: 404 });
}
return NextResponse.json(user);
}Middleware
// middleware.ts
import { clerkMiddleware, createRouteMatcher } from '@clerk/nextjs/server';
const isPublicRoute = createRouteMatcher(['/', '/sign-in(.*)', '/blog(.*)']);
export default clerkMiddleware(async (auth, req) => {
if (!isPublicRoute(req)) {
await auth.protect();
}
});Quick Reference
Dynamic Routes: [slug] (single), [...slug] (catch-all), [[...slug]] (optional)
Route Groups: (name) - organize without affecting URLs
Navigation: <Link> (client), router.push() (programmatic), redirect() (server)
Special Files: page.tsx, layout.tsx, loading.tsx, error.tsx, route.ts
How is this guide ?
Last updated on