Skip to main content

Dynamic Routes

Dynamic routes allow you to capture values from the URL and use them in your route handlers. They're perfect for RESTful APIs where you need to work with specific resources identified by IDs or slugs.

What Are Dynamic Routes?​

Dynamic routes use square brackets [param] in folder names to capture URL segments as parameters. These captured values become available in your route handler through req.params.

Syntax​

Create a folder with square brackets to define a dynamic segment:

[paramName]

The paramName will be the key in req.params object.

Basic Example​

Folder Structure​

api/
products/
[id]/
route.ts → /api/products/[id]

Route Handler​

api/products/[id]/route.ts
import type { BurgerRequest } from "burger-api";

export function GET(req: BurgerRequest) {
// Access the captured parameter
const productId = req.params.id;

return Response.json({
message: `Fetching product with ID: ${productId}`,
productId,
});
}

export function DELETE(req: BurgerRequest) {
const productId = req.params.id;

// Delete logic here...

return Response.json({
message: `Product ${productId} deleted successfully`,
});
}

Example Requests​

GET /api/products/123
→ req.params.id = "123"

GET /api/products/abc-def
→ req.params.id = "abc-def"

DELETE /api/products/456
→ req.params.id = "456"

Multiple Dynamic Segments​

You can have multiple dynamic segments in the same route:

Folder Structure​

api/
users/
[userId]/
posts/
[postId]/
route.ts → /api/users/[userId]/posts/[postId]

Route Handler​

api/users/[userId]/posts/[postId]/route.ts
import type { BurgerRequest } from "burger-api";

export function GET(req: BurgerRequest) {
const { userId, postId } = req.params;

return Response.json({
message: `Fetching post ${postId} from user ${userId}`,
userId,
postId,
});
}

Example Request​

GET /api/users/42/posts/789
→ req.params.userId = "42"
→ req.params.postId = "789"

Accessing Parameters​

Parameters are always available as strings in the req.params object:

export function GET(req: BurgerRequest) {
// Direct destructuring
const { id } = req.params;

// Or access by key
const userId = req.params.userId;

// Convert to number if needed
const numericId = parseInt(id, 10);

return Response.json({ id, numericId });
}

Validation with Zod​

For type safety and validation, use Zod schemas:

api/products/[id]/route.ts
import type { BurgerRequest } from "burger-api";
import { z } from "zod";

// Define validation schema
export const schema = {
get: {
params: z.object({
id: z.string().uuid(), // Validate as UUID
}),
},
delete: {
params: z.object({
id: z.string().min(1), // Ensure non-empty
}),
},
};

export function GET(req: BurgerRequest) {
// Access validated params
const { id } = req.validated.params;

// TypeScript knows 'id' is a valid UUID string
return Response.json({
message: "Product found",
productId: id,
});
}

export function DELETE(req: BurgerRequest) {
const { id } = req.validated.params;

return Response.json({
message: `Product ${id} deleted`,
});
}

Advanced Validation​

import { z } from "zod";

export const schema = {
get: {
params: z.object({
// Must be numeric string
userId: z.string().regex(/^\d+$/),
// Slug format: lowercase, hyphens only
slug: z.string().regex(/^[a-z0-9-]+$/),
}),
},
};

Route Matching Priority​

Understanding Priority

BurgerAPI matches routes in this order:

  1. Static routes - Exact path matches (e.g., /products/featured)
  2. Dynamic routes - Single parameter patterns (e.g., /products/[id])
  3. Wildcard routes - Catch-all patterns (e.g., /products/[...])

Dynamic routes are checked after static routes but before wildcard routes.

Priority Example​

Given these routes:

api/products/featured/route.ts → Static
api/products/[id]/route.ts → Dynamic
api/products/[...]/route.ts → Wildcard

Request matching:

GET /api/products/featured
→ Matches: Static route (exact match)

GET /api/products/123
→ Matches: Dynamic route (not "featured", so dynamic [id] matches)

GET /api/products/123/reviews
→ Matches: Wildcard route (multiple segments, only wildcard can handle)

Common Patterns​

1. Resource CRUD Operations​

api/
users/
[userId]/
route.ts → GET, PUT, DELETE /api/users/[userId]
api/users/[userId]/route.ts
import type { BurgerRequest } from "burger-api";
import { z } from "zod";

export const schema = {
get: {
params: z.object({ userId: z.string().min(1) }),
},
put: {
params: z.object({ userId: z.string().min(1) }),
body: z.object({
name: z.string(),
email: z.string().email(),
}),
},
delete: {
params: z.object({ userId: z.string().min(1) }),
},
};

export function GET(req: BurgerRequest) {
const { userId } = req.validated.params;
return Response.json({ userId, action: "fetch" });
}

export async function PUT(req: BurgerRequest) {
const { userId } = req.validated.params;
const { name, email } = req.validated.body;

return Response.json({
userId,
action: "update",
data: { name, email },
});
}

export function DELETE(req: BurgerRequest) {
const { userId } = req.validated.params;
return Response.json({ userId, action: "delete" });
}

2. Nested Resources​

api/
projects/
[projectId]/
tasks/
[taskId]/
route.ts → /api/projects/[projectId]/tasks/[taskId]
export function GET(req: BurgerRequest) {
const { projectId, taskId } = req.params;

return Response.json({
message: `Task ${taskId} in project ${projectId}`,
projectId,
taskId,
});
}

3. Slug-Based Routes​

api/
blog/
[slug]/
route.ts → /api/blog/[slug]
api/blog/[slug]/route.ts
import type { BurgerRequest } from "burger-api";
import { z } from "zod";

export const schema = {
get: {
params: z.object({
slug: z.string().regex(/^[a-z0-9-]+$/),
}),
},
};

export function GET(req: BurgerRequest) {
const { slug } = req.validated.params;

return Response.json({
slug,
title: `Blog post: ${slug}`,
content: "...",
});
}

4. Combining with Route Groups​

api/
(authenticated)/
profile/
[section]/
route.ts → /api/profile/[section]

The (authenticated) group is ignored, but [section] still captures the parameter.

Best Practices​

1. Use Descriptive Parameter Names​

// ✅ Good: Clear and specific
api/users/[userId]/route.ts
api/products/[productId]/route.ts
api/posts/[slug]/route.ts

// ❌ Avoid: Generic names
api/users/[id]/route.ts // Which ID?
api/items/[x]/route.ts // What is 'x'?

2. Always Validate Parameters​

// ✅ Good: Validate with Zod
export const schema = {
get: {
params: z.object({
userId: z.string().uuid(),
}),
},
};

// ❌ Avoid: Trusting raw params
export function GET(req: BurgerRequest) {
const id = req.params.userId; // Could be anything!
// Use without validation...
}

3. Handle Invalid Parameters​

export function GET(req: BurgerRequest) {
const { userId } = req.params;

// Validate format
if (!/^\d+$/.test(userId)) {
return Response.json(
{ error: "Invalid user ID format" },
{ status: 400 }
);
}

const numericId = parseInt(userId, 10);

// Check if resource exists
const user = await findUser(numericId);
if (!user) {
return Response.json(
{ error: "User not found" },
{ status: 404 }
);
}

return Response.json(user);
}

4. Type Safety with TypeScript​

import type { BurgerRequest } from "burger-api";

// Define expected params type
type Params = {
userId: string;
postId: string;
};

export function GET(req: BurgerRequest) {
const { userId, postId } = req.params as Params;
// Now TypeScript knows the shape of params
}

Limitations​

Cannot Mix with Wildcards​

You cannot have both dynamic [param] and wildcard [...] folders at the same directory level:

// ❌ This won't work:
api/
products/
[id]/
route.ts
[...]/
route.ts

// ✅ Choose one pattern per level:
api/
products/
[id]/
route.ts

Parameters Are Always Strings​

All parameters come in as strings. Convert them to other types as needed:

export function GET(req: BurgerRequest) {
const idStr = req.params.id; // "123" (string)
const idNum = parseInt(idStr, 10); // 123 (number)
const isActive = req.params.active === "true"; // boolean
}

Next Steps​

Now that you understand dynamic routes, explore other routing patterns:

Dynamic routes are essential for RESTful APIs. They let you build flexible endpoints that work with any resource identifier, making your API scalable and maintainable.