
nextjs-dynamic-routes-params
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).
- Installs
- 303
- Last 24h
- —
- Trending rank
- —
- Security
- Not audited
Install
Works with every agent the skills CLI supports.
npx skills add https://github.com/wsimmonds/claude-nextjs-skills --skill nextjs-dynamic-routes-params- 01
Install
Run the command in your project. The CLI asks which agents to install for.
- 02
Describe the task
Ask your agent for something this skill covers. It loads nextjs-dynamic-routes-params automatically when relevant.
- 03
Review the result
The agent follows the SKILL.md steps, scripts and references bundled with the skill.
SKILL.md
The instructions your agent reads when the skill is triggered.
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
paramsprop 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
- “Show a product page that loads whichever product ID appears in the URL” →
app/[id]/page.tsxorapp/products/[id]/page.tsx - “Render a blog article based on its slug” →
app/blog/[slug]/page.tsxorapp/[slug]/page.tsx - “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✅ (notapp/products/[id]) - "Show user profile" →
app/[userId]/page.tsx✅ (notapp/users/[userId]) - "Display blog post" →
app/[slug]/page.tsx✅ (notapp/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:
-
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:
/123for any resource,/abc-deffor slugs - Pattern: The ID/slug is the only identifier needed
- When in doubt, choose this!
-
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)
-
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!
// ✅ 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>;
}
// ❌ 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:
// 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)
// 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:
- Use
useParams()hook:
'use client';
import { useParams } from 'next/navigation';
export function ProductClient() {
const params = useParams<{ id: string }>();
const id = params.id;
// Use the id...
}
- Pass params from Server Component:
// 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
// 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
// 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>
);
}
// Generate static params for SSG
export async function generateStaticParams() {
const posts = await getAllPosts();
return posts.map((post) => ({
slug: post.slug,
}));
}
Pattern 3: Nested Resources
// app/users/[userId]/posts/[postId]/page.tsx
interface PageProps {
params: Promise<{
userId: string;
postId: string;
}>;
}
export default async function UserPost({ params }: PageProps) {
const { userId, postId } = await params;
const [user, post] = await Promise.all([
getUserById(userId),
getPostById(postId),
]);
return (
<div>
<h1>{post.title}</h1>
<p>By {user.name}</p>
<div>{post.content}</div>
</div>
);
}
Pattern 4: Catch-All Routes
// app/docs/[...slug]/page.tsx - Matches /docs/a, /docs/a/b, /docs/a/b/c
interface PageProps {
params: Promise<{
slug: string[]; // Array of path segments
}>;
}
export default async function DocsPage({ params }: PageProps) {
const { slug } = await params;
const path = slug.join('/');
const doc = await getDocByPath(path);
return <div>{doc.content}</div>;
}
// app/shop/[[...slug]]/page.tsx - Optional catch-all
// Matches /shop, /shop/electronics, /shop/electronics/phones
interface PageProps {
params: Promise<{
slug?: string[]; // Optional array
}>;
}
export default async function ShopPage({ params }: PageProps) {
const { slug = [] } = await params;
if (slug.length === 0) {
return <ShopHomepage />;
}
return <CategoryPage category={slug.join('/')} />;
}
TypeScript Best Practices
Type Safety for Params
// Define params type separately for reusability
type ProductPageParams = { id: string };
interface ProductPageProps {
params: Promise<ProductPageParams>;
}
export default async function ProductPage({ params }: ProductPageProps) {
const { id } = await params;
// id is typed as string
}
// Reuse in generateMetadata
export async function generateMetadata({ params }: ProductPageProps) {
const { id } = await params;
const product = await getProduct(id);
return { title: product.name };
}
Multiple Dynamic Segments
type PostPageParams = {
category: string;
slug: string;
};
interface PostPageProps {
params: Promise<PostPageParams>;
searchParams: Promise<{ [key: string]: string | string[] | undefined }>;
}
export default async function PostPage({ params, searchParams }: PostPageProps) {
const { category, slug } = await params;
const { view } = await searchParams;
// All properly typed
}
Common Pitfalls and Solutions
Pitfall 1: Forgetting params is a Promise (Next.js 15+)
// ❌ WRONG
export default async function Page({ params }: { params: { id: string } }) {
const product = await getProduct(params.id); // Error: params is Promise
}
// ✅ CORRECT
export default async function Page({ params }: { params: Promise<{ id: string }> }) {
const { id } = await params;
const product = await getProduct(id);
}
Pitfall 2: Using params in Client Components without useParams
// ❌ WRONG - params prop doesn't exist in Client Components
'use client';
export default function ClientPage({ params }) { // undefined!
return <div>{params.id}</div>;
}
// ✅ CORRECT
'use client';
import { useParams } from 'next/navigation';
export default function ClientPage() {
const params = useParams<{ id: string }>();
return <div>{params.id}</div>;
}
Pitfall 3: Over-nesting Routes
// ❌ UNNECESSARY NESTING
// app/products/product/[id]/page.tsx
// URL: /products/product/123
// ✅ SIMPLER
// app/products/[id]/page.tsx
// URL: /products/123
// OR even simpler if product is the main resource:
// app/[id]/page.tsx
// URL: /123
Pitfall 4: Not Handling Invalid IDs
// ✅ ROBUST
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 => {
if (!res.ok) throw new Error('Product not found');
return res.json();
});
if (!product) {
notFound(); // Shows 404 page
}
return <div>{product.name}</div>;
}
Decision Guide
When you need to create a dynamic route, ask:
-
What's the URL structure?
- Single ID:
[id] - Category + ID:
category/[id] - Hierarchical:
[category]/[id] - Flexible paths:
[...slug]
- Single ID:
-
Is this a Server or Client Component?
- Server: Use
paramsprop (await it in Next.js 15+) - Client: Use
useParams()hook
- Server: Use
-
Do I need the simplest structure?
- When in doubt, use fewer nesting levels
- Top-level routes are simpler and more direct
-
Am I on Next.js 15+?
- Yes:
paramsisPromise<{...}> - No:
paramsis{...}
- Yes:
Examples: Real-World Scenarios
E-commerce Product Page
// app/products/[id]/page.tsx
import { notFound } from 'next/navigation';
interface Product {
id: string;
name: string;
price: number;
description: string;
}
async function getProduct(id: string): Promise<Product | null> {
const res = await fetch(`https://api.example.com/products/${id}`, {
next: { revalidate: 60 }, // Revalidate every 60s
});
if (!res.ok) return null;
return res.json();
}
export default async function ProductPage({
params,
}: {
params: Promise<{ id: string }>;
}) {
const { id } = await params;
const product = await getProduct(id);
if (!product) {
notFound();
}
return (
<div>
<h1>{product.name}</h1>
<p>${product.price}</p>
<p>{product.description}</p>
</div>
);
}
export async function generateMetadata({
params,
}: {
params: Promise<{ id: string }>;
}) {
const { id } = await params;
const product = await getProduct(id);
return {
title: product?.name ?? 'Product Not Found',
description: product?.description,
};
}
Documentation Site with Nested Paths
// app/docs/[...slug]/page.tsx
interface DocPageProps {
params: Promise<{ slug: string[] }>;
}
export default async function DocPage({ params }: DocPageProps) {
const { slug } = await params;
const path = slug.join('/');
const doc = await getDocByPath(path);
if (!doc) {
notFound();
}
return (
<article className="prose">
<h1>{doc.title}</h1>
<div dangerouslySetInnerHTML={{ __html: doc.html }} />
</article>
);
}
export async function generateStaticParams() {
const docs = await getAllDocs();
return docs.map((doc) => ({
slug: doc.path.split('/'),
}));
}
Checklist for Dynamic Routes
Before implementing a dynamic route, verify:
- Route structure is as simple as possible (default to top-level unless told otherwise)
- Not inferring route nesting from resource type names
- Route structure matches URL requirements (not over-nested)
-
paramsis typed asPromise<{...}>for Next.js 15+ -
paramsis awaited before accessing properties (Next.js 15+) - Error handling for invalid IDs/slugs (use
notFound()) - TypeScript types are properly defined
- Using Server Component unless client interactivity needed
- Consider
generateStaticParams()for SSG if applicable - Metadata generation if SEO matters
Quick Reference
| Scenario | Route Structure | Params Access |
|---|---|---|
| Single resource by ID | app/[id]/page.tsx | const { id } = await params |
| Category + resource | app/category/[id]/page.tsx | const { id } = await params |
| Blog with slugs | app/blog/[slug]/page.tsx | const { slug } = await params |
| Nested resources | app/[cat]/[id]/page.tsx | const { cat, id } = await params |
| Flexible paths | app/docs/[...slug]/page.tsx | const { slug } = await params (slug is array) |
| Optional paths | app/[[...slug]]/page.tsx | const { slug = [] } = await params |
| Client Component | Use useParams() hook | const params = useParams<{ id: string }>() |
Remember: Dynamic routes in Next.js are file-system based. The folder structure with [brackets] creates the dynamic segments, and the params prop (or useParams() hook) provides access to those values.
FAQ
What does the nextjs-dynamic-routes-params skill do?
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).
How do I install nextjs-dynamic-routes-params?
Run npx skills add https://github.com/wsimmonds/claude-nextjs-skills --skill nextjs-dynamic-routes-params in your project. The skills CLI detects the agents you use and copies the skill into the right folder. For Claude Code you can also place the skill folder at ~/.claude/skills/nextjs-dynamic-routes-params/ (personal) or .claude/skills/nextjs-dynamic-routes-params/ (project).
Which AI agents can use nextjs-dynamic-routes-params?
Any agent that supports the open Agent Skills format (a folder with a SKILL.md file), including Claude Code, Codex, Cursor, GitHub Copilot, Gemini CLI, Windsurf, Cline, OpenCode and more.
Is nextjs-dynamic-routes-params safe to install?
No partner security audit is available yet. Skills can run commands and scripts, so read the SKILL.md and any bundled files before installing.
Who maintains nextjs-dynamic-routes-params?
It is published by wsimmonds in wsimmonds/claude-nextjs-skills. It has 303 recorded installs.
More from wsimmonds/claude-nextjs-skills
nextjs-app-router-fundamentals
wsimmonds/claude-nextjs-skills
Guide for working with Next.js App Router (Next.js 13+). Use when migrating from Pages Router to App Router, creating layouts, implementing routing, handling metadata, or building Next.js 13+ applications. Activates for App Router migration, layout creation, routing patterns, or Next.js 13+ development tasks.
nextjs-server-client-components
wsimmonds/claude-nextjs-skills
Guide for choosing between Server Components and Client Components in Next.js App Router. CRITICAL for useSearchParams (requires Suspense + 'use client'), navigation (Link, redirect, useRouter), cookies/headers access, and 'use client' directive. Activates when prompt mentions useSearchParams, Suspense, navigation, routing, Link component, redirect, pathname, searchParams, cookies, headers, async components, or 'use client'. Essential for avoiding mixing server/client APIs.
vercel-ai-sdk
wsimmonds/claude-nextjs-skills
Open the skill to read its SKILL.md instructions.
nextjs-advanced-routing
wsimmonds/claude-nextjs-skills
Guide for advanced Next.js App Router patterns including Route Handlers, Parallel Routes, Intercepting Routes, Server Actions, error boundaries, draft mode, and streaming with Suspense. CRITICAL for server actions (action.ts, actions.ts files, 'use server' directive), setting cookies from client components, and form handling. Use when requirements involve server actions, form submissions, cookies, mutations, API routes, route.ts, parallel routes, intercepting routes, or streaming. Essential for separating server actions from client components.
nextjs-client-cookie-pattern
wsimmonds/claude-nextjs-skills
Pattern for client components calling server actions to set cookies in Next.js. Covers the two-file pattern of a client component with user interaction (onClick, form submission) that calls a server action to modify cookies. Use when building features like authentication, preferences, or session management where client-side triggers need to set/modify server-side cookies.
nextjs-use-search-params-suspense
wsimmonds/claude-nextjs-skills
Pattern for using useSearchParams hook with Suspense boundary in Next.js. Covers the required combination of 'use client' directive and Suspense wrapper when accessing URL query parameters in client components. Use when building search interfaces, filters, pagination, or any feature that needs to read/manipulate URL query parameters client-side.
Popular frontend & design skills
setup-matt-pocock-skills
mattpocock/skills
Configure this repo for the engineering skills: set up its issue tracker, triage label vocabulary, and domain doc layout. Run once before first use of the other engineering skills.
frontend-design
anthropics/skills
Guidance for distinctive, intentional visual design when building new UI or reshaping an existing one. Helps with aesthetic direction, typography, and making choices that don't read as templated defaults.
hyperframes-animation
heygen-com/hyperframes
All animation knowledge for HyperFrames — atomic motion rules, multi-phase scene blueprints, scene transitions, broader motion-design techniques, AND the seven runtime adapters (GSAP default, plus Lottie, Three.js, Anime.js, CSS keyframes, Web Animations API, TypeGPU). Use for any motion or animation task: pick 2-4 rules and compose, or load a blueprint, or look up runtime-specific API (e.g. GSAP eases / Lottie player / Three.js mixer). Also covers auditing an existing composition's choreography (animation map) and 24 named text-animation effects. HyperFrames-native: single paused timeline, seek-safe, deterministic.
prototype
mattpocock/skills
Build a throwaway prototype to answer a design question. Use when the user wants to sanity-check whether a state model or logic feels right, or explore what a UI should look like.
hyperframes-core
heygen-com/hyperframes
The HyperFrames composition contract — build one renderable project. Use for composition structure, the data- timing attributes, class="clip", tracks, sub-compositions, variables, framework-owned media playback, deterministic-render rules, and validation. Read before writing composition HTML.
vercel-react-best-practices
vercel-labs/agent-skills
React and Next.js performance optimization guidelines from Vercel Engineering. This skill should be used when writing, reviewing, or refactoring React/Next.js code to ensure optimal performance patterns. Triggers on tasks involving React components, Next.js pages, data fetching, bundle optimization, or performance improvements.