Claude
Skills
Sign in
Back

nextjs-dynamic-routes-params

Included with Lifetime
$97 forever

Guide for Next.js App Router dynamic routes and pathname parameters. Use when building pages that depend on URL segments (IDs, slugs, nested paths), accessing the `params` prop, or fetching resources by identifier. Helps avoid over-nesting by defaulting to the simplest route structure (e.g., `app/[id]` instead of `app/products/[id]` unless the URL calls for it).

Web Dev

What this skill does


# Next.js Dynamic Routes and Pathname Parameters

## When to Use This Skill

Use this skill when:
- Creating dynamic route segments (e.g., blog/[slug], users/[id])
- Accessing URL pathname parameters in Server or Client Components
- Building pages that fetch data based on route parameters
- Implementing catch-all or optional catch-all routes
- Working with the `params` prop in page.tsx, layout.tsx, or route.ts

## ⚠️ RECOGNIZING WHEN YOU NEED DYNAMIC ROUTES

**Look for requirements that tie data to the URL path.**

Create a dynamic segment (`[param]`) whenever the UI depends on part of the pathname. Typical signals include:
- Details pages that reference “the item’s ID/slug from the URL”
- Copy that calls out path segments (e.g., `/products/{id}`, `/blog/{slug}`)
- Requirements to fetch data “based on whichever resource is being visited”
- Navigation flows where one page links to `/something/{identifier}`

**✅ Dynamic route response**
```
Requirement: display product information based on whichever ID appears in the URL
Implementation: app/[id]/page.tsx
Access parameter with: const { id } = await params;
```

**❌ Static-page response**
```
Implementation: app/page.tsx  ← cannot access per-path identifiers
```

**Example requirements that lead to dynamic routes**
1. “Show a product page that loads whichever product ID appears in the URL” → `app/[id]/page.tsx` or `app/products/[id]/page.tsx`
2. “Render a blog article based on its slug” → `app/blog/[slug]/page.tsx` or `app/[slug]/page.tsx`
3. “Support nested docs such as /docs/getting-started/installation” → `app/docs/[...slug]/page.tsx`

**Core rule:** If data varies with a URL segment, the folder name needs matching brackets.

## ⚠️ CRITICAL: Avoid Over-Engineering Route Structure

**MOST COMMON MISTAKE:** Adding unnecessary nesting to routes.

**Default Rule:** When creating a dynamic route, use `app/[id]/page.tsx` or `app/[slug]/page.tsx` unless:
- The URL structure is explicitly specified (e.g., "create route at /products/[id]")
- You're building multiple resource types that need namespacing
- The requirements clearly show a nested URL structure

**Do NOT infer nesting from resource names:**
- "Fetch a product by ID" → `app/[id]/page.tsx` ✅ (not `app/products/[id]`)
- "Show user profile" → `app/[userId]/page.tsx` ✅ (not `app/users/[userId]`)
- "Display blog post" → `app/[slug]/page.tsx` ✅ (not `app/blog/[slug]`)

**Only nest when explicitly told:**
- "Create a route at /blog/[slug]" → `app/blog/[slug]/page.tsx` ✅
- "Products should be at /products/[id]" → `app/products/[id]/page.tsx` ✅

## Core Concepts

### Dynamic Route Syntax

Next.js uses **folder names with square brackets** to create dynamic route segments:

```
app/
├── [id]/page.tsx              # Matches /123, /abc, etc.
├── blog/[slug]/page.tsx       # Matches /blog/hello-world
├── shop/[category]/[id]/page.tsx  # Matches /shop/electronics/123
└── docs/[...slug]/page.tsx    # Matches /docs/a, /docs/a/b, /docs/a/b/c
```

**Key Principle:** The folder structure IS the route structure.

### Route Structure Decision Tree

**CRITICAL RULE: Do NOT infer route structure from resource type names!**

Just because you're fetching a "product" or "user" doesn't mean you need `/products/[id]` or `/users/[id]`. **Unless explicitly told otherwise, prefer the simplest structure.**

**When deciding on route structure:**

1. **Top-level dynamic route** (`app/[id]/page.tsx`)
   - **DEFAULT CHOICE** - Use this unless specifically told otherwise
   - Use when the resource IS the primary entity
   - Use when only ID-based routing is needed
   - Examples: `/123` for any resource, `/abc-def` for slugs
   - Pattern: The ID/slug is the only identifier needed
   - **When in doubt, choose this!**

2. **Nested dynamic route** (`app/category/[id]/page.tsx`)
   - **ONLY use when explicitly required by the URL structure**
   - Use when you're told "create a /products/[id] route"
   - Use when the URL itself needs the category prefix
   - Examples: `/products/123`, `/blog/my-post` (when specified)
   - Pattern: Category + identifier (when both are required)

3. **Multi-segment dynamic** (`app/[cat]/[id]/page.tsx`)
   - Use when hierarchy matters
   - Examples: `/shop/electronics/123`
   - Pattern: Multiple levels of categorization

**⚠️ COMMON MISTAKE:** Creating `app/products/[id]/page.tsx` when you should create `app/[id]/page.tsx`

❌ **WRONG:** "Fetch a product by ID" → `app/products/[id]/page.tsx`
✅ **CORRECT:** "Fetch a product by ID" → `app/[id]/page.tsx`

❌ **WRONG:** "Create a dynamic route for users" → `app/users/[userId]/page.tsx`
✅ **CORRECT:** "Create a dynamic route for users" → `app/[userId]/page.tsx`

**Only add the category prefix when:**
- The requirement explicitly says "at /products/..." or similar
- You're building multiple resource types that need namespacing
- The URL structure is specified in requirements

## Accessing Pathname Parameters

### In Server Components (page.tsx, layout.tsx)

**CRITICAL: In Next.js 15+, `params` is a Promise and must be awaited!**

```typescript
// ✅ CORRECT - Next.js 15+
export default async function ProductPage({
  params,
}: {
  params: Promise<{ id: string }>;
}) {
  const { id } = await params;

  const product = await fetch(`https://api.example.com/products/${id}`)
    .then(res => res.json());

  return <div>{product.name}</div>;
}
```

```typescript
// ❌ WRONG - Treating params as synchronous object (Next.js 15+)
export default async function ProductPage({
  params,
}: {
  params: { id: string };  // Missing Promise wrapper
}) {
  const product = await fetch(`https://api.example.com/products/${params.id}`);
  // This will fail because params is a Promise!
}
```

**For Next.js 14 and earlier:**
```typescript
// Next.js 14 - params is synchronous
export default async function ProductPage({
  params,
}: {
  params: { id: string };
}) {
  const product = await fetch(`https://api.example.com/products/${params.id}`)
    .then(res => res.json());

  return <div>{product.name}</div>;
}
```

### In Route Handlers (route.ts)

```typescript
// app/api/products/[id]/route.ts
export async function GET(
  request: Request,
  { params }: { params: Promise<{ id: string }> }
) {
  const { id } = await params;

  const product = await db.products.findById(id);
  return Response.json(product);
}
```

### In Client Components

**You CANNOT access `params` directly in Client Components.** Instead:

1. **Use `useParams()` hook:**
```typescript
'use client';

import { useParams } from 'next/navigation';

export function ProductClient() {
  const params = useParams<{ id: string }>();
  const id = params.id;

  // Use the id...
}
```

2. **Pass params from Server Component:**
```typescript
// app/products/[id]/page.tsx (Server Component)
export default async function ProductPage({
  params,
}: {
  params: Promise<{ id: string }>;
}) {
  const { id } = await params;
  return <ProductClient productId={id} />;
}

// components/ProductClient.tsx
'use client';

export function ProductClient({ productId }: { productId: string }) {
  // Use productId...
}
```

## Common Patterns

### Pattern 1: Simple ID-Based Page

```typescript
// app/[id]/page.tsx - Top-level dynamic route
interface PageProps {
  params: Promise<{ id: string }>;
}

export default async function ItemPage({ params }: PageProps) {
  const { id } = await params;

  const item = await fetch(`https://api.example.com/items/${id}`)
    .then(res => res.json());

  return (
    <div>
      <h1>{item.title}</h1>
      <p>{item.description}</p>
    </div>
  );
}
```

### Pattern 2: Blog Post with Slug

```typescript
// app/blog/[slug]/page.tsx
interface PageProps {
  params: Promise<{ slug: string }>;
}

export default async function BlogPost({ params }: PageProps) {
  const { slug } = await params;

  const post = await getPostBySlug(slug);

  return (
    <article>
      <h1>{post.title}</h1>
      <div dangerouslySetInnerHTML={{ __html: post.content }} />
    </article>
  );
}

/

Related in Web Dev