Skip to main content

Routing & Navigation

App Router, Pages Router, dynamic routes, navigation, and URL handling in Next.js applications.

App Router (Next.js 13+)

Basic Routing

// app/page.tsx → /
export default function HomePage() {
return <h1>Home Page</h1>;
}

// app/about/page.tsx → /about
export default function AboutPage() {
return <h1>About Page</h1>;
}

// app/blog/page.tsx → /blog
export default function BlogPage() {
return <h1>Blog Page</h1>;
}

// app/dashboard/settings/page.tsx → /dashboard/settings
export default function SettingsPage() {
return <h1>Settings Page</h1>;
}

Dynamic Routes

// app/blog/[slug]/page.tsx → /blog/hello-world
export default function BlogPost({ params }: { params: { slug: string } }) {
return <h1>Blog Post: {params.slug}</h1>;
}

// app/user/[id]/page.tsx → /user/123
export default function UserProfile({ params }: { params: { id: string } }) {
return <h1>User ID: {params.id}</h1>;
}

// app/product/[...slug]/page.tsx → /product/electronics/phones/iphone
export default function ProductPage({ params }: { params: { slug: string[] } }) {
return <h1>Product: {params.slug.join('/')}</h1>;
}

// app/shop/[[...slug]]/page.tsx → /shop, /shop/category, /shop/category/item
export default function ShopPage({ params }: { params: { slug?: string[] } }) {
const path = params.slug ? params.slug.join('/') : 'all';
return <h1>Shop: {path}</h1>;
}

Route Groups

// app/(auth)/login/page.tsx → /login
// app/(auth)/register/page.tsx → /register
// app/(auth)/layout.tsx → Layout for auth pages

// app/(dashboard)/analytics/page.tsx → /analytics
// app/(dashboard)/settings/page.tsx → /settings
// app/(dashboard)/layout.tsx → Layout for dashboard pages

// Route groups don't affect URL structure

Parallel Routes

// app/dashboard/@analytics/page.tsx
export default function Analytics() {
return <div>Analytics Panel</div>;
}

// app/dashboard/@team/page.tsx
export default function Team() {
return <div>Team Panel</div>;
}

// app/dashboard/layout.tsx
export default function DashboardLayout({
children,
analytics,
team,
}: {
children: React.ReactNode;
analytics: React.ReactNode;
team: React.ReactNode;
}) {
return (
<div>
{children}
<div className="dashboard-panels">
{analytics}
{team}
</div>
</div>
);
}

Intercepting Routes

// app/photo/[id]/page.tsx → /photo/123
export default function PhotoPage({ params }: { params: { id: string } }) {
return <div>Photo {params.id}</div>;
}

// app/(.)photo/[id]/page.tsx → Intercepts /photo/123
export default function PhotoModal({ params }: { params: { id: string } }) {
return (
<div className="modal">
<div>Photo {params.id} in modal</div>
</div>
);
}

Pages Router (Legacy)

Basic Routing

// pages/index.tsx → /
export default function HomePage() {
return <h1>Home Page</h1>;
}

// pages/about.tsx → /about
export default function AboutPage() {
return <h1>About Page</h1>;
}

// pages/blog/index.tsx → /blog
export default function BlogPage() {
return <h1>Blog Page</h1>;
}

// pages/dashboard/settings.tsx → /dashboard/settings
export default function SettingsPage() {
return <h1>Settings Page</h1>;
}

Dynamic Routes

// pages/blog/[slug].tsx → /blog/hello-world
import { useRouter } from 'next/router';

export default function BlogPost() {
const router = useRouter();
const { slug } = router.query;

return <h1>Blog Post: {slug}</h1>;
}

// pages/user/[id].tsx → /user/123
export default function UserProfile() {
const router = useRouter();
const { id } = router.query;

return <h1>User ID: {id}</h1>;
}

// pages/product/[...slug].tsx → /product/electronics/phones/iphone
export default function ProductPage() {
const router = useRouter();
const { slug } = router.query;

return <h1>Product: {Array.isArray(slug) ? slug.join('/') : slug}</h1>;
}
import Link from 'next/link';

export default function Navigation() {
return (
<nav>
{/* Basic navigation */}
<Link href="/">Home</Link>
<Link href="/about">About</Link>
<Link href="/blog">Blog</Link>

{/* Dynamic routes */}
<Link href="/blog/hello-world">Blog Post</Link>
<Link href={`/user/${userId}`}>User Profile</Link>

{/* Query parameters */}
<Link href="/search?q=nextjs&category=framework">Search</Link>

{/* Hash fragments */}
<Link href="/docs#getting-started">Getting Started</Link>

{/* External links */}
<Link href="https://nextjs.org" target="_blank" rel="noopener">
Next.js Docs
</Link>

{/* Conditional navigation */}
{isLoggedIn ? (
<Link href="/dashboard">Dashboard</Link>
) : (
<Link href="/login">Login</Link>
)}

{/* Custom styling */}
<Link href="/about" className="nav-link">
About
</Link>

{/* Prefetch control */}
<Link href="/heavy-page" prefetch={false}>
Heavy Page
</Link>
</nav>
);
}

Programmatic Navigation

// App Router
import { useRouter } from 'next/navigation';

export default function NavigationExample() {
const router = useRouter();

const handleNavigation = () => {
// Navigate to route
router.push('/dashboard');

// Navigate with query params
router.push('/search?q=nextjs');

// Replace current route
router.replace('/new-page');

// Go back
router.back();

// Go forward
router.forward();

// Refresh current route
router.refresh();
};

return (
<div>
<button onClick={handleNavigation}>Navigate</button>
</div>
);
}
// Pages Router
import { useRouter } from 'next/router';

export default function NavigationExample() {
const router = useRouter();

const handleNavigation = () => {
// Navigate to route
router.push('/dashboard');

// Navigate with query params
router.push({
pathname: '/search',
query: { q: 'nextjs', category: 'framework' }
});

// Replace current route
router.replace('/new-page');

// Go back
router.back();

// Reload page
router.reload();
};

return (
<div>
<button onClick={handleNavigation}>Navigate</button>
</div>
);
}

Route Handlers (API Routes)

App Router API Routes

// app/api/users/route.ts
import { NextRequest, NextResponse } from 'next/server';

export async function GET(request: NextRequest) {
const searchParams = request.nextUrl.searchParams;
const query = searchParams.get('query');

return NextResponse.json({ users: [], query });
}

export async function POST(request: NextRequest) {
const body = await request.json();

return NextResponse.json({ message: 'User created', data: body });
}

export async function PUT(request: NextRequest) {
const body = await request.json();

return NextResponse.json({ message: 'User updated', data: body });
}

export async function DELETE(request: NextRequest) {
return NextResponse.json({ message: 'User deleted' });
}
// app/api/users/[id]/route.ts
export async function GET(
request: NextRequest,
{ params }: { params: { id: string } }
) {
const id = params.id;

return NextResponse.json({ user: { id, name: 'John Doe' } });
}

export async function PUT(
request: NextRequest,
{ params }: { params: { id: string } }
) {
const id = params.id;
const body = await request.json();

return NextResponse.json({ message: `User ${id} updated`, data: body });
}

Pages Router API Routes

// pages/api/users.ts
import type { NextApiRequest, NextApiResponse } from 'next';

export default function handler(req: NextApiRequest, res: NextApiResponse) {
if (req.method === 'GET') {
res.status(200).json({ users: [] });
} else if (req.method === 'POST') {
const body = req.body;
res.status(201).json({ message: 'User created', data: body });
} else {
res.setHeader('Allow', ['GET', 'POST']);
res.status(405).end(`Method ${req.method} Not Allowed`);
}
}
// pages/api/users/[id].ts
export default function handler(req: NextApiRequest, res: NextApiResponse) {
const { id } = req.query;

if (req.method === 'GET') {
res.status(200).json({ user: { id, name: 'John Doe' } });
} else if (req.method === 'PUT') {
const body = req.body;
res.status(200).json({ message: `User ${id} updated`, data: body });
} else {
res.setHeader('Allow', ['GET', 'PUT']);
res.status(405).end(`Method ${req.method} Not Allowed`);
}
}

Route Parameters & Query Strings

Accessing Route Parameters

// App Router
export default function ProductPage({ params }: { params: { id: string } }) {
return <div>Product ID: {params.id}</div>;
}

// Pages Router
import { useRouter } from 'next/router';

export default function ProductPage() {
const router = useRouter();
const { id } = router.query;

return <div>Product ID: {id}</div>;
}

Accessing Query Parameters

// App Router
import { useSearchParams } from 'next/navigation';

export default function SearchPage() {
const searchParams = useSearchParams();
const query = searchParams.get('q');
const category = searchParams.get('category');

return (
<div>
<p>Query: {query}</p>
<p>Category: {category}</p>
</div>
);
}

// Pages Router
import { useRouter } from 'next/router';

export default function SearchPage() {
const router = useRouter();
const { q, category } = router.query;

return (
<div>
<p>Query: {q}</p>
<p>Category: {category}</p>
</div>
);
}

Layout and Templates

App Router Layouts

// app/layout.tsx - Root Layout
export default function RootLayout({
children,
}: {
children: React.ReactNode;
}) {
return (
<html lang="en">
<body>
<header>Global Header</header>
<main>{children}</main>
<footer>Global Footer</footer>
</body>
</html>
);
}

// app/dashboard/layout.tsx - Nested Layout
export default function DashboardLayout({
children,
}: {
children: React.ReactNode;
}) {
return (
<div className="dashboard">
<aside>Dashboard Sidebar</aside>
<main>{children}</main>
</div>
);
}

Templates

// app/template.tsx - Template (re-renders on navigation)
export default function Template({ children }: { children: React.ReactNode }) {
return <div className="template-wrapper">{children}</div>;
}

// app/dashboard/template.tsx - Nested Template
export default function DashboardTemplate({
children,
}: {
children: React.ReactNode;
}) {
return (
<div className="dashboard-template">
<div className="dashboard-animation">{children}</div>
</div>
);
}

Special Files

Loading UI

// app/loading.tsx - Global loading
export default function Loading() {
return (
<div className="loading">
<div className="spinner"></div>
<p>Loading...</p>
</div>
);
}

// app/dashboard/loading.tsx - Dashboard loading
export default function DashboardLoading() {
return (
<div className="dashboard-loading">
<div className="skeleton-loader"></div>
</div>
);
}

Error UI

// app/error.tsx - Global error boundary
'use client';

export default function Error({
error,
reset,
}: {
error: Error & { digest?: string };
reset: () => void;
}) {
return (
<div className="error-boundary">
<h2>Something went wrong!</h2>
<p>{error.message}</p>
<button onClick={() => reset()}>Try again</button>
</div>
);
}

// app/global-error.tsx - Global error (root level)
'use client';

export default function GlobalError({
error,
reset,
}: {
error: Error & { digest?: string };
reset: () => void;
}) {
return (
<html>
<body>
<h2>Something went wrong!</h2>
<button onClick={() => reset()}>Try again</button>
</body>
</html>
);
}

Not Found

// app/not-found.tsx - Global 404
export default function NotFound() {
return (
<div className="not-found">
<h2>Not Found</h2>
<p>Could not find requested resource</p>
</div>
);
}

// app/blog/not-found.tsx - Blog section 404
export default function BlogNotFound() {
return (
<div className="blog-not-found">
<h2>Blog Post Not Found</h2>
<p>The blog post you're looking for doesn't exist</p>
</div>
);
}

Advanced Routing

Route Handlers with Middleware

// app/api/protected/route.ts
import { NextRequest, NextResponse } from 'next/server';

export async function GET(request: NextRequest) {
const token = request.headers.get('authorization');

if (!token) {
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
}

return NextResponse.json({ data: 'Protected data' });
}

Custom Route Matching

// middleware.ts
import { NextResponse } from 'next/server';
import type { NextRequest } from 'next/server';

export function middleware(request: NextRequest) {
// Redirect old URLs
if (request.nextUrl.pathname.startsWith('/old-blog')) {
return NextResponse.redirect(new URL('/blog', request.url));
}

// Authentication check
if (request.nextUrl.pathname.startsWith('/dashboard')) {
const token = request.cookies.get('auth-token');
if (!token) {
return NextResponse.redirect(new URL('/login', request.url));
}
}

return NextResponse.next();
}

export const config = {
matcher: ['/dashboard/:path*', '/old-blog/:path*'],
};

Quick Reference

App Router Structure

  • app/page.tsx - Route page
  • app/layout.tsx - Route layout
  • app/loading.tsx - Loading UI
  • app/error.tsx - Error UI
  • app/not-found.tsx - 404 page
  • app/template.tsx - Template

Dynamic Routes

  • [id] - Single dynamic segment
  • [...slug] - Catch-all segment
  • [[...slug]] - Optional catch-all segment
  • <Link href="/path"> - Client-side navigation
  • router.push('/path') - Programmatic navigation
  • router.replace('/path') - Replace current route
  • router.back() - Go back in history

Route Groups

  • (auth) - Route group (doesn't affect URL)
  • @slot - Parallel route slot
  • (.)modal - Intercept route

API Routes

  • route.ts - API route handler (App Router)
  • api/route.ts - API endpoint (Pages Router)
  • Supports GET, POST, PUT, DELETE, etc.

Best Practices

  • Use App Router for new projects
  • Implement proper error boundaries
  • Add loading states for better UX
  • Use route groups for organization
  • Implement proper SEO with metadata
  • Use middleware for authentication and redirects