DEV Community

Cover image for Mastering Next.js Routing: Dynamic Routes, Route Groups, and Parallel Routes
jordan wilfry
jordan wilfry

Posted on

Mastering Next.js Routing: Dynamic Routes, Route Groups, and Parallel Routes

Unlocking the Full Power of Next.js 15's File-Based Routing System

Next.js has always been celebrated for its intuitive file-based routing, but the App Router in Next.js 15 takes this concept to unprecedented levels of sophistication. While the basics of creating a page by adding a file seem simple, developers often struggle with more advanced routing patterns: organizing complex applications with route groups, handling dynamic segments efficiently, implementing parallel routes for sophisticated layouts, and intercepting routes for modal experiences.

This comprehensive guide takes you from fundamental routing concepts to advanced patterns that power production applications. You'll learn how to structure your file system for optimal organization, create dynamic product pages that scale, build dashboard layouts with parallel data streams, implement modal interactions using intercepting routes, and master route handlers for API endpoints. Whether you're building a simple blog or a complex e-commerce platform, you'll gain the routing expertise needed to architect maintainable Next.js applications.

Prerequisites

Before exploring advanced routing patterns, ensure you have:

  • Next.js 15 installed - npx create-next-app@latest my-app
  • Basic Next.js App Router knowledge - Understanding of page.tsx and layout.tsx
  • TypeScript fundamentals - Interfaces, types, and basic syntax
  • React knowledge - Components, props, and basic hooks
  • File system concepts - Understanding folders, paths, and directory structures
  • HTTP methods - GET, POST, PUT, DELETE basics
  • Code editor - VS Code with Next.js snippets extension recommended

The Foundation: File-Based Routing Basics

Next.js uses your file system structure to define routes automatically. Each folder in the app directory represents a URL segment, and special files define the UI for that route.

Core Routing Files

app/
├── page.tsx              // Route: /
├── layout.tsx            // Applies to / and all children
├── loading.tsx           // Loading UI for /
├── error.tsx             // Error UI for /
├── not-found.tsx         // 404 UI for /
└── about/
    └── page.tsx          // Route: /about
Enter fullscreen mode Exit fullscreen mode

Let's build a basic routing structure:

// app/page.tsx
export default function HomePage() {
  return (
    <div>
      <h1>Welcome to Our Store</h1>
      <p>Discover amazing products</p>
    </div>
  );
}
Enter fullscreen mode Exit fullscreen mode
// app/layout.tsx
import { ReactNode } from 'react';
import './globals.css';

export default function RootLayout({ children }: { children: ReactNode }) {
  return (
    <html lang="en">
      <body>
        <nav>
          <a href="/">Home</a>
          <a href="/products">Products</a>
          <a href="/about">About</a>
        </nav>
        <main>{children}</main>
      </body>
    </html>
  );
}
Enter fullscreen mode Exit fullscreen mode

Understanding Route Segments

Each folder creates a URL segment:

app/
├── blog/
   ├── page.tsx          // /blog
   └── authors/
       └── page.tsx      // /blog/authors
└── products/
    ├── page.tsx          // /products
    └── categories/
        └── page.tsx      // /products/categories
Enter fullscreen mode Exit fullscreen mode

Dynamic Routes: Building Scalable URL Patterns

Dynamic routes allow you to create pages for dynamic data without creating individual files for each item.

Single Dynamic Segment

Create a dynamic segment using square brackets:

app/
└── products/
    └── [id]/
        └── page.tsx      // Matches /products/1, /products/abc, etc.
Enter fullscreen mode Exit fullscreen mode
// app/products/[id]/page.tsx
interface ProductPageProps {
  params: {
    id: string;
  };
}

async function getProduct(id: string) {
  const res = await fetch(`https://api.example.com/products/${id}`, {
    next: { revalidate: 3600 }
  });

  if (!res.ok) throw new Error('Failed to fetch product');
  return res.json();
}

export default async function ProductPage({ params }: ProductPageProps) {
  const product = await getProduct(params.id);

  return (
    <div className="product-container">
      <h1>{product.name}</h1>
      <img src={product.image} alt={product.name} />
      <p className="price">${product.price}</p>
      <p className="description">{product.description}</p>
      <button>Add to Cart</button>
    </div>
  );
}
Enter fullscreen mode Exit fullscreen mode

Generating Static Params

For static generation with dynamic routes, use generateStaticParams:

// app/products/[id]/page.tsx
export async function generateStaticParams() {
  const products = await fetch('https://api.example.com/products').then(
    res => res.json()
  );

  return products.map((product: any) => ({
    id: product.id.toString(),
  }));
}

// This generates static pages at build time for all products
export default async function ProductPage({ params }: ProductPageProps) {
  const product = await getProduct(params.id);
  return <div>{product.name}</div>;
}
Enter fullscreen mode Exit fullscreen mode

Multiple Dynamic Segments

Create complex URL patterns with multiple dynamic segments:

app/
└── blog/
    └── [category]/
        └── [slug]/
            └── page.tsx  // Matches /blog/tech/nextjs-guide
Enter fullscreen mode Exit fullscreen mode
// app/blog/[category]/[slug]/page.tsx
interface BlogPostProps {
  params: {
    category: string;
    slug: string;
  };
}

export async function generateStaticParams() {
  const posts = await fetch('https://api.example.com/posts').then(
    res => res.json()
  );

  return posts.map((post: any) => ({
    category: post.category,
    slug: post.slug,
  }));
}

export default async function BlogPost({ params }: BlogPostProps) {
  const post = await fetch(
    `https://api.example.com/posts/${params.category}/${params.slug}`
  ).then(res => res.json());

  return (
    <article>
      <div className="breadcrumb">
        <a href="/blog">Blog</a> / 
        <a href={`/blog/${params.category}`}>{params.category}</a> / 
        <span>{post.title}</span>
      </div>
      <h1>{post.title}</h1>
      <div dangerouslySetInnerHTML={{ __html: post.content }} />
    </article>
  );
}
Enter fullscreen mode Exit fullscreen mode

Catch-All Segments

Match multiple path segments with catch-all routes:

app/
└── shop/
    └── [...slug]/
        └── page.tsx      // Matches /shop/a, /shop/a/b, /shop/a/b/c
Enter fullscreen mode Exit fullscreen mode
// app/shop/[...slug]/page.tsx
interface ShopPageProps {
  params: {
    slug: string[];  // Array of path segments
  };
}

export default async function ShopPage({ params }: ShopPageProps) {
  // /shop/electronics/phones/iphone -> ['electronics', 'phones', 'iphone']
  const { slug } = params;

  // Determine what to show based on depth
  if (slug.length === 1) {
    // Category page: /shop/electronics
    return <CategoryPage category={slug[0]} />;
  } else if (slug.length === 2) {
    // Subcategory page: /shop/electronics/phones
    return <SubcategoryPage category={slug[0]} subcategory={slug[1]} />;
  } else if (slug.length === 3) {
    // Product page: /shop/electronics/phones/iphone
    return <ProductDetailPage path={slug} />;
  }

  return <div>Shop</div>;
}
Enter fullscreen mode Exit fullscreen mode

Optional Catch-All Segments

Match zero or more segments:

app/
└── docs/
    └── [[...slug]]/
        └── page.tsx      // Matches /docs, /docs/a, /docs/a/b
Enter fullscreen mode Exit fullscreen mode
// app/docs/[[...slug]]/page.tsx
interface DocsPageProps {
  params: {
    slug?: string[];  // Optional array
  };
}

export default async function DocsPage({ params }: DocsPageProps) {
  const slug = params.slug || [];

  // /docs -> Show documentation home
  if (slug.length === 0) {
    return <DocsHome />;
  }

  // /docs/api/authentication -> Show specific doc
  const docPath = slug.join('/');
  const doc = await fetchDoc(docPath);

  return (
    <div className="docs-container">
      <aside>
        <DocsSidebar currentPath={docPath} />
      </aside>
      <article>
        <h1>{doc.title}</h1>
        <div dangerouslySetInnerHTML={{ __html: doc.content }} />
      </article>
    </div>
  );
}
Enter fullscreen mode Exit fullscreen mode

Route Groups: Organizing Your Application

Route groups let you organize routes without affecting the URL structure. Create a route group by wrapping a folder name in parentheses.

Basic Route Groups

app/
├── (marketing)/
   ├── about/
      └── page.tsx      // URL: /about (not /marketing/about)
   ├── contact/
      └── page.tsx      // URL: /contact
   └── layout.tsx        // Applies to marketing pages only
└── (shop)/
    ├── products/
       └── page.tsx      // URL: /products (not /shop/products)
    └── layout.tsx        // Applies to shop pages only
Enter fullscreen mode Exit fullscreen mode
// app/(marketing)/layout.tsx
export default function MarketingLayout({ 
  children 
}: { 
  children: React.ReactNode 
}) {
  return (
    <div>
      <header className="marketing-header">
        <nav>
          <a href="/">Home</a>
          <a href="/about">About</a>
          <a href="/contact">Contact</a>
        </nav>
      </header>
      {children}
      <footer className="marketing-footer">
        <p>&copy; 2026 Our Company</p>
      </footer>
    </div>
  );
}
Enter fullscreen mode Exit fullscreen mode
// app/(shop)/layout.tsx
export default function ShopLayout({ 
  children 
}: { 
  children: React.ReactNode 
}) {
  return (
    <div>
      <header className="shop-header">
        <nav>
          <a href="/products">Products</a>
          <a href="/cart">Cart</a>
          <a href="/account">Account</a>
        </nav>
      </header>
      <div className="shop-content">
        <aside className="shop-sidebar">
          <FilterPanel />
        </aside>
        <main>{children}</main>
      </div>
    </div>
  );
}
Enter fullscreen mode Exit fullscreen mode

Multiple Root Layouts

Route groups enable multiple root layouts in your application:

app/
├── (auth)/
   ├── layout.tsx        // Auth-specific root layout
   ├── login/
      └── page.tsx
   └── register/
       └── page.tsx
└── (dashboard)/
    ├── layout.tsx        // Dashboard-specific root layout
    ├── page.tsx
    └── settings/
        └── page.tsx
Enter fullscreen mode Exit fullscreen mode
// app/(auth)/layout.tsx
export default function AuthLayout({ children }: { children: React.ReactNode }) {
  return (
    <html lang="en">
      <body className="auth-body">
        <div className="auth-container">
          <div className="auth-card">
            {children}
          </div>
        </div>
      </body>
    </html>
  );
}
Enter fullscreen mode Exit fullscreen mode
// app/(dashboard)/layout.tsx
export default function DashboardLayout({ 
  children 
}: { 
  children: React.ReactNode 
}) {
  return (
    <html lang="en">
      <body className="dashboard-body">
        <nav className="dashboard-nav">
          <a href="/dashboard">Dashboard</a>
          <a href="/dashboard/analytics">Analytics</a>
          <a href="/dashboard/settings">Settings</a>
        </nav>
        <main className="dashboard-main">
          {children}
        </main>
      </body>
    </html>
  );
}
Enter fullscreen mode Exit fullscreen mode

Organizing by Feature

Use route groups to organize by feature while maintaining flat URLs:

app/
├── (products)/
   ├── products/
      └── page.tsx          // /products
   ├── products/[id]/
      └── page.tsx          // /products/[id]
   └── categories/
       └── page.tsx          // /categories
└── (users)/
    ├── profile/
       └── page.tsx          // /profile
    └── settings/
        └── page.tsx          // /settings
Enter fullscreen mode Exit fullscreen mode

Parallel Routes: Loading Multiple Pages Simultaneously

Parallel routes allow you to render multiple pages in the same layout simultaneously, each with independent loading and error states.

Creating Parallel Routes

Define parallel routes using the @folder convention:

app/
└── dashboard/
    ├── @analytics/
       ├── page.tsx
       └── loading.tsx
    ├── @notifications/
       ├── page.tsx
       └── loading.tsx
    ├── layout.tsx
    └── page.tsx
Enter fullscreen mode Exit fullscreen mode
// app/dashboard/layout.tsx
export default function DashboardLayout({
  children,
  analytics,
  notifications,
}: {
  children: React.ReactNode;
  analytics: React.ReactNode;
  notifications: React.ReactNode;
}) {
  return (
    <div className="dashboard-grid">
      <div className="dashboard-main">
        {children}
      </div>
      <div className="dashboard-analytics">
        {analytics}
      </div>
      <div className="dashboard-notifications">
        {notifications}
      </div>
    </div>
  );
}
Enter fullscreen mode Exit fullscreen mode
// app/dashboard/@analytics/page.tsx
async function getAnalytics() {
  const res = await fetch('https://api.example.com/analytics', {
    next: { revalidate: 300 }  // Revalidate every 5 minutes
  });
  return res.json();
}

export default async function AnalyticsSlot() {
  const data = await getAnalytics();

  return (
    <div className="analytics-panel">
      <h2>Analytics</h2>
      <div className="metrics">
        <div className="metric">
          <span className="metric-label">Page Views</span>
          <span className="metric-value">{data.pageViews}</span>
        </div>
        <div className="metric">
          <span className="metric-label">Users</span>
          <span className="metric-value">{data.users}</span>
        </div>
      </div>
    </div>
  );
}
Enter fullscreen mode Exit fullscreen mode
// app/dashboard/@analytics/loading.tsx
export default function AnalyticsLoading() {
  return (
    <div className="analytics-panel">
      <h2>Analytics</h2>
      <div className="loading-skeleton">
        <div className="skeleton-line"></div>
        <div className="skeleton-line"></div>
      </div>
    </div>
  );
}
Enter fullscreen mode Exit fullscreen mode

Conditional Rendering with Parallel Routes

Show different content based on user state:

// app/dashboard/layout.tsx
import { getUser } from '@/lib/auth';

export default async function DashboardLayout({
  children,
  analytics,
  team,
}: {
  children: React.ReactNode;
  analytics: React.ReactNode;
  team: React.ReactNode;
}) {
  const user = await getUser();

  return (
    <div className="dashboard">
      {children}
      {analytics}
      {user.role === 'admin' && team}
    </div>
  );
}
Enter fullscreen mode Exit fullscreen mode

Default Parallel Routes

Provide a default when no matching page exists:

app/
└── dashboard/
    ├── @analytics/
       ├── default.tsx       // Fallback for @analytics
       └── page.tsx
    └── layout.tsx
Enter fullscreen mode Exit fullscreen mode
// app/dashboard/@analytics/default.tsx
export default function AnalyticsDefault() {
  return (
    <div className="analytics-panel">
      <p>Analytics not available for this view</p>
    </div>
  );
}
Enter fullscreen mode Exit fullscreen mode

Intercepting Routes: Creating Modal Experiences

Intercepting routes allow you to load a route within the current layout while still preserving the URL for sharing and refreshing.

Intercepting Route Conventions

  • (.) - Match segments on the same level
  • (..) - Match segments one level above
  • (..)(..) - Match segments two levels above
  • (...) - Match segments from the root

Building a Photo Gallery with Modals

app/
└── photos/
    ├── page.tsx              // Gallery view
    ├── [id]/
       └── page.tsx          // Full photo page
    └── (.)[id]/
        └── page.tsx          // Intercepted modal view
Enter fullscreen mode Exit fullscreen mode
// app/photos/page.tsx
import Link from 'next/link';

async function getPhotos() {
  const res = await fetch('https://api.example.com/photos');
  return res.json();
}

export default async function PhotosPage() {
  const photos = await getPhotos();

  return (
    <div className="photo-grid">
      {photos.map((photo: any) => (
        <Link key={photo.id} href={`/photos/${photo.id}`}>
          <img src={photo.thumbnail} alt={photo.title} />
        </Link>
      ))}
    </div>
  );
}
Enter fullscreen mode Exit fullscreen mode
// app/photos/(.)[id]/page.tsx - Modal view
import { Modal } from '@/components/Modal';

async function getPhoto(id: string) {
  const res = await fetch(`https://api.example.com/photos/${id}`);
  return res.json();
}

export default async function PhotoModal({ 
  params 
}: { 
  params: { id: string } 
}) {
  const photo = await getPhoto(params.id);

  return (
    <Modal>
      <img src={photo.url} alt={photo.title} className="modal-image" />
      <h2>{photo.title}</h2>
      <p>{photo.description}</p>
    </Modal>
  );
}
Enter fullscreen mode Exit fullscreen mode
// app/photos/[id]/page.tsx - Full page view
async function getPhoto(id: string) {
  const res = await fetch(`https://api.example.com/photos/${id}`);
  return res.json();
}

export default async function PhotoPage({ 
  params 
}: { 
  params: { id: string } 
}) {
  const photo = await getPhoto(params.id);

  return (
    <div className="photo-page">
      <img src={photo.url} alt={photo.title} />
      <div className="photo-details">
        <h1>{photo.title}</h1>
        <p>{photo.description}</p>
        <div className="photo-meta">
          <span>Uploaded: {photo.uploadDate}</span>
          <span>Views: {photo.views}</span>
        </div>
      </div>
    </div>
  );
}
Enter fullscreen mode Exit fullscreen mode
// components/Modal.tsx
'use client';

import { useRouter } from 'next/navigation';
import { useEffect, useRef } from 'react';

export function Modal({ children }: { children: React.ReactNode }) {
  const router = useRouter();
  const dialogRef = useRef<HTMLDialogElement>(null);

  useEffect(() => {
    dialogRef.current?.showModal();
  }, []);

  const handleClose = () => {
    router.back();
  };

  return (
    <dialog ref={dialogRef} onClose={handleClose} className="photo-modal">
      <button onClick={handleClose} className="close-button">×</button>
      {children}
    </dialog>
  );
}
Enter fullscreen mode Exit fullscreen mode

E-commerce Product Quick View

app/
└── products/
    ├── page.tsx              // Product listing
    ├── [id]/
       └── page.tsx          // Full product page
    └── (.)[id]/
        └── page.tsx          // Quick view modal
Enter fullscreen mode Exit fullscreen mode
// app/products/(.)[id]/page.tsx
import { Modal } from '@/components/Modal';
import { AddToCartButton } from '@/components/AddToCartButton';

async function getProduct(id: string) {
  const res = await fetch(`https://api.example.com/products/${id}`);
  return res.json();
}

export default async function ProductQuickView({ 
  params 
}: { 
  params: { id: string } 
}) {
  const product = await getProduct(params.id);

  return (
    <Modal>
      <div className="quick-view">
        <div className="quick-view-image">
          <img src={product.image} alt={product.name} />
        </div>
        <div className="quick-view-details">
          <h2>{product.name}</h2>
          <p className="price">${product.price}</p>
          <p className="description">{product.shortDescription}</p>
          <AddToCartButton productId={product.id} />
          <a href={`/products/${product.id}`} className="view-full">
            View Full Details →
          </a>
        </div>
      </div>
    </Modal>
  );
}
Enter fullscreen mode Exit fullscreen mode

Route Handlers: Building API Endpoints

Route handlers allow you to create custom API endpoints using Web Request and Response APIs.

Basic Route Handler

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

export async function GET(request: NextRequest) {
  const products = await fetchProducts();

  return NextResponse.json({
    success: true,
    data: products,
  });
}

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

  const newProduct = await createProduct(body);

  return NextResponse.json({
    success: true,
    data: newProduct,
  }, { status: 201 });
}
Enter fullscreen mode Exit fullscreen mode

Dynamic Route Handlers

// app/api/products/[id]/route.ts
import { NextRequest, NextResponse } from 'next/server';

interface RouteParams {
  params: {
    id: string;
  };
}

export async function GET(
  request: NextRequest,
  { params }: RouteParams
) {
  const product = await fetchProduct(params.id);

  if (!product) {
    return NextResponse.json(
      { success: false, error: 'Product not found' },
      { status: 404 }
    );
  }

  return NextResponse.json({
    success: true,
    data: product,
  });
}

export async function PATCH(
  request: NextRequest,
  { params }: RouteParams
) {
  const body = await request.json();
  const updatedProduct = await updateProduct(params.id, body);

  return NextResponse.json({
    success: true,
    data: updatedProduct,
  });
}

export async function DELETE(
  request: NextRequest,
  { params }: RouteParams
) {
  await deleteProduct(params.id);

  return NextResponse.json({
    success: true,
    message: 'Product deleted',
  });
}
Enter fullscreen mode Exit fullscreen mode

Accessing Request Data

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

export async function GET(request: NextRequest) {
  // Get query parameters
  const searchParams = request.nextUrl.searchParams;
  const query = searchParams.get('q');
  const page = searchParams.get('page') || '1';
  const limit = searchParams.get('limit') || '10';

  // Get headers
  const authToken = request.headers.get('authorization');

  // Get cookies
  const cookies = request.cookies;
  const sessionId = cookies.get('sessionId')?.value;

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

  const results = await searchProducts({
    query,
    page: parseInt(page),
    limit: parseInt(limit),
  });

  return NextResponse.json({
    success: true,
    data: results,
    pagination: {
      page: parseInt(page),
      limit: parseInt(limit),
      total: results.total,
    },
  });
}
Enter fullscreen mode Exit fullscreen mode

Setting Response Headers and Cookies

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

export async function POST(request: NextRequest) {
  const { email, password } = await request.json();

  const user = await authenticateUser(email, password);

  if (!user) {
    return NextResponse.json(
      { error: 'Invalid credentials' },
      { status: 401 }
    );
  }

  const token = generateToken(user);

  const response = NextResponse.json({
    success: true,
    user: {
      id: user.id,
      email: user.email,
      name: user.name,
    },
  });

  // Set cookie
  response.cookies.set('token', token, {
    httpOnly: true,
    secure: process.env.NODE_ENV === 'production',
    sameSite: 'lax',
    maxAge: 60 * 60 * 24 * 7, // 7 days
  });

  // Set custom headers
  response.headers.set('X-User-Id', user.id);

  return response;
}
Enter fullscreen mode Exit fullscreen mode

Streaming Responses

// app/api/stream/route.ts
export async function GET() {
  const encoder = new TextEncoder();

  const stream = new ReadableStream({
    async start(controller) {
      for (let i = 0; i < 10; i++) {
        await new Promise(resolve => setTimeout(resolve, 1000));
        controller.enqueue(encoder.encode(`data: Message ${i}\n\n`));
      }
      controller.close();
    },
  });

  return new Response(stream, {
    headers: {
      'Content-Type': 'text/event-stream',
      'Cache-Control': 'no-cache',
      'Connection': 'keep-alive',
    },
  });
}
Enter fullscreen mode Exit fullscreen mode

Best Practices Section

✅ Dos

Use route groups for organization - Keep your file structure clean and maintainable without affecting URLs:

// ✅ Good - Organized by feature
app/
├── (auth)/
   ├── login/
   └── register/
└── (dashboard)/
    ├── analytics/
    └── settings/
Enter fullscreen mode Exit fullscreen mode

Leverage generateStaticParams for dynamic routes - Pre-render pages at build time for better performance:

// ✅ Good - Static generation
export async function generateStaticParams() {
  const posts = await fetchAllPosts();
  return posts.map(post => ({ slug: post.slug }));
}
Enter fullscreen mode Exit fullscreen mode

Implement proper loading and error states - Use loading.tsx and error.tsx files for better UX:

// app/products/loading.tsx
export default function Loading() {
  return <ProductGridSkeleton />;
}

// app/products/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>
  );
}
Enter fullscreen mode Exit fullscreen mode

Use intercepting routes for modal experiences - Preserve URLs while showing modals for better sharing and navigation:

// ✅ Good - Modal with shareable URL
app/
└── photos/
    ├── [id]/page.tsx         // Full page
    └── (.)[id]/page.tsx      // Modal intercept
Enter fullscreen mode Exit fullscreen mode

Validate and sanitize route handler inputs - Always validate incoming data:

// ✅ Good - Input validation
export async function POST(request: NextRequest) {
  const body = await request.json();

  if (!body.email || !isValidEmail(body.email)) {
    return NextResponse.json(
      { error: 'Invalid email' },
      { status: 400 }
    );
  }

  // Process valid data
}
Enter fullscreen mode Exit fullscreen mode

❌ Don'ts

Don't create unnecessary nesting - Keep routes as flat as possible:

// ❌ Bad - Too nested
app/pages/products/categories/electronics/page.tsx

// ✅ Good - Flatter structure
app/categories/[slug]/page.tsx
Enter fullscreen mode Exit fullscreen mode

Don't forget to handle errors in route handlers - Always include error handling:

// ❌ Bad - No error handling
export async function GET() {
  const data = await fetchData(); // Can throw
  return NextResponse.json(data);
}

// ✅ Good - Proper error handling
export async function GET() {
  try {
    const data = await fetchData();
    return NextResponse.json({ success: true, data });
  } catch (error) {
    console.error('API Error:', error);
    return NextResponse.json(
      { success: false, error: 'Internal server error' },
      { status: 500 }
    );
  }
}
Enter fullscreen mode Exit fullscreen mode

Don't use route handlers for server-side rendering - Use Server Components instead:

// ❌ Bad - Using API route for data
'use client';
export default function Page() {
  const [data, setData] = useState(null);
  useEffect(() => {
    fetch('/api/data').then(r => r.json()).then(setData);
  }, []);
  return <div>{data?.title}</div>;
}

// ✅ Good - Server Component
export default async function Page() {
  const data = await fetchData();
  return <div>{data.title}</div>;
}
Enter fullscreen mode Exit fullscreen mode

Don't expose sensitive data in parallel routes - Remember all slots are exposed to the client:

// ❌ Bad - Sensitive data in parallel route
app/dashboard/@admin/page.tsx  // Admin data visible to all users

// ✅ Good - Check permissions in layout
export default async function Layout({ admin }) {
  const user = await getUser();
  return user.isAdmin ? admin : null;
}
Enter fullscreen mode Exit fullscreen mode

Common Pitfalls

Pitfall 1: Forgetting default.tsx for parallel routes

When navigating to a route that doesn't have a matching parallel route page, Next.js needs a fallback:

// ❌ Bad - Missing default
app/dashboard/
├── @analytics/page.tsx
└── layout.tsx

// Navigate to /dashboard/settings -> Error!

// ✅ Good - Include default
app/dashboard/
├── @analytics/   ├── page.tsx
   └── default.tsx           // Fallback
└── layout.tsx
Enter fullscreen mode Exit fullscreen mode

Pitfall 2: Incorrect intercepting route conventions

// ❌ Bad - Wrong intercept level
app/photos/
└── (..)[id]/page.tsx         // Goes up one level, not same level

// ✅ Good - Same level intercept
app/photos/
└── (.)[id]/page.tsx          // Correct for same level
Enter fullscreen mode Exit fullscreen mode

Pitfall
3: Not handling the back button in modals

// ❌ Bad - No back button handling
export function Modal({ children }) {
  return <div className="modal">{children}</div>;
}

// ✅ Good - Proper router integration
'use client';
import { useRouter } from 'next/navigation';

export function Modal({ children }) {
  const router = useRouter();
  return (
    <div className="modal" onClick={() => router.back()}>
      {children}
    </div>
  );
}
Enter fullscreen mode Exit fullscreen mode

Conclusion

Mastering Next.js routing unlocks the full potential of the App Router architecture. From basic file-based routing to advanced patterns like parallel and intercepting routes, you now have the knowledge to build sophisticated, scalable applications with clean and maintainable code structures.

The key takeaways: use dynamic routes with generateStaticParams for optimal performance, organize complex applications with route groups without affecting URLs, leverage parallel routes for loading multiple data sources simultaneously, implement intercepting routes for seamless modal experiences, and create robust API endpoints with route handlers. Each pattern serves specific use cases, and choosing the right combination makes the difference between a good application and a great one.

Remember that routing is not just about URLs—it's about creating intuitive user experiences, organizing your codebase effectively, and optimizing performance. Start with simple patterns and gradually incorporate advanced techniques as your application grows. The file-based routing system may seem magical at first, but understanding these patterns gives you complete control over how your application behaves.

Resources

GitHub Repository

Official Documentation

Related Articles

  • "Next.js 15 App Router: Complete Guide to Server and Client Components" - Understand component architecture
  • "Building Authentication Systems in Next.js with Route Protection" - Secure your routes
  • "Next.js Middleware: Advanced Request Handling and Route Protection" - Route-level logic

Meta Description: Master Next.js 15 routing with dynamic routes, route groups, parallel routes, and intercepting routes. Complete guide with e-commerce examples and best practices.

Tags: #nextjs #routing #webdev #react #typescript #apirouter #tutorial

Top comments (0)