RESTful APIs operate on resources identified by URLs. Dynamic route parameters let you create endpoints like /api/posts/[id] that handle individual resources. Combine with HTTP methods (GET, PUT, PATCH, DELETE) to build complete CRUD APIs that follow REST principles. Master dynamic API routes and you'll build scalable, maintainable APIs!
Basic Dynamic Route Parameter
Single Resource Endpoint
// GET single post by ID
export async function GET(
request: Request,
{ params }: { params: { id: string } }
) {
const id = params.id;
const post = await db.posts.findUnique({
where: { id },
include: {
author: {
select: { id: true, name: true },
},
},
});
if (!post) {
return Response.json(
{ error: 'Post not found' },
{ status: 404 }
);
}
return Response.json(post);
}
// GET /api/posts/123
// Returns: { id: '123', title: '...', author: {...} }
// ✅ params from second argument
// ✅ Type-safe with TypeScript
// ✅ 404 if not found
// ✅ Include related dataAccessing Route Parameters
// Route Handler signature
export async function GET(
request: Request, // First parameter: Request
context: { params: { id: string } } // Second parameter: Context with params
) {
// Access parameter
const id = context.params.id;
// Alternative: destructure directly
const { params } = context;
const id = params.id;
// Or in one line
const { params: { id } } = context;
}
// TypeScript types for params
interface Params {
id: string;
// All params are strings
}
export async function GET(
request: Request,
{ params }: { params: Params }
) {
const id = params.id;
}
// ✅ Always receive request first
// ✅ params in second argument
// ✅ All params are strings (convert if needed)Complete CRUD Operations
Collection Route (List and Create)
import { z } from 'zod';
const createPostSchema = z.object({
title: z.string().min(1).max(200),
content: z.string().min(10),
published: z.boolean().default(false),
});
// GET - List all posts
export async function GET(request: Request) {
const { searchParams } = new URL(request.url);
const page = parseInt(searchParams.get('page') || '1');
const limit = parseInt(searchParams.get('limit') || '10');
const posts = await db.posts.findMany({
skip: (page - 1) * limit,
take: limit,
orderBy: { createdAt: 'desc' },
});
const total = await db.posts.count();
return Response.json({
posts,
pagination: {
page,
limit,
total,
totalPages: Math.ceil(total / limit),
},
});
}
// POST - Create new post
export async function POST(request: Request) {
const body = await request.json();
const result = createPostSchema.safeParse(body);
if (!result.success) {
return Response.json(
{ error: 'Validation failed', details: result.error.flatten() },
{ status: 400 }
);
}
const post = await db.posts.create({
data: {
...result.data,
authorId: 'user-123', // From auth in production
},
});
return Response.json(post, { status: 201 });
}
// GET /api/posts - List posts
// POST /api/posts - Create post
// ✅ Collection endpoints
// ✅ GET for listing
// ✅ POST for creatingResource Route (Get, Update, Delete)
import { z } from 'zod';
const updatePostSchema = z.object({
title: z.string().min(1).max(200).optional(),
content: z.string().min(10).optional(),
published: z.boolean().optional(),
});
// GET - Retrieve single post
export async function GET(
request: Request,
{ params }: { params: { id: string } }
) {
const post = await db.posts.findUnique({
where: { id: params.id },
include: {
author: { select: { id: true, name: true } },
comments: { orderBy: { createdAt: 'desc' } },
},
});
if (!post) {
return Response.json(
{ error: 'Post not found' },
{ status: 404 }
);
}
return Response.json(post);
}
// PUT - Full update (replace entire resource)
export async function PUT(
request: Request,
{ params }: { params: { id: string } }
) {
const body = await request.json();
// Validate all required fields for full update
if (!body.title || !body.content) {
return Response.json(
{ error: 'Title and content required for full update' },
{ status: 400 }
);
}
try {
const post = await db.posts.update({
where: { id: params.id },
data: {
title: body.title,
content: body.content,
published: body.published ?? false,
},
});
return Response.json(post);
} catch (error) {
return Response.json(
{ error: 'Post not found' },
{ status: 404 }
);
}
}
// PATCH - Partial update (update specific fields)
export async function PATCH(
request: Request,
{ params }: { params: { id: string } }
) {
const body = await request.json();
const result = updatePostSchema.safeParse(body);
if (!result.success) {
return Response.json(
{ error: 'Validation failed', details: result.error.flatten() },
{ status: 400 }
);
}
try {
const post = await db.posts.update({
where: { id: params.id },
data: result.data,
});
return Response.json(post);
} catch (error) {
return Response.json(
{ error: 'Post not found' },
{ status: 404 }
);
}
}
// DELETE - Remove resource
export async function DELETE(
request: Request,
{ params }: { params: { id: string } }
) {
try {
await db.posts.delete({
where: { id: params.id },
});
// Option 1: 204 No Content (no body)
return new Response(null, { status: 204 });
// Option 2: 200 with success message
// return Response.json({ message: 'Post deleted successfully' });
} catch (error) {
return Response.json(
{ error: 'Post not found' },
{ status: 404 }
);
}
}
// GET /api/posts/123 - Get post
// PUT /api/posts/123 - Full update
// PATCH /api/posts/123 - Partial update
// DELETE /api/posts/123 - Delete post
// ✅ Complete CRUD
// ✅ PUT for full updates
// ✅ PATCH for partial updates
// ✅ DELETE returns 204
// ✅ All operations validatedNested Dynamic Routes
Nested Resource Example
// GET - List comments for a specific post
export async function GET(
request: Request,
{ params }: { params: { postId: string } }
) {
const { searchParams } = new URL(request.url);
const page = parseInt(searchParams.get('page') || '1');
const limit = parseInt(searchParams.get('limit') || '20');
// Verify post exists
const post = await db.posts.findUnique({
where: { id: params.postId },
});
if (!post) {
return Response.json(
{ error: 'Post not found' },
{ status: 404 }
);
}
// Get comments
const comments = await db.comments.findMany({
where: { postId: params.postId },
skip: (page - 1) * limit,
take: limit,
orderBy: { createdAt: 'desc' },
include: {
author: { select: { id: true, name: true } },
},
});
const total = await db.comments.count({
where: { postId: params.postId },
});
return Response.json({
comments,
pagination: { page, limit, total },
});
}
// POST - Create comment on post
export async function POST(
request: Request,
{ params }: { params: { postId: string } }
) {
const body = await request.json();
// Validate
if (!body.content || body.content.trim().length < 1) {
return Response.json(
{ error: 'Comment content required' },
{ status: 400 }
);
}
// Verify post exists
const post = await db.posts.findUnique({
where: { id: params.postId },
});
if (!post) {
return Response.json(
{ error: 'Post not found' },
{ status: 404 }
);
}
// Create comment
const comment = await db.comments.create({
data: {
content: body.content,
postId: params.postId,
authorId: 'user-123', // From auth
},
include: {
author: { select: { id: true, name: true } },
},
});
return Response.json(comment, { status: 201 });
}
// GET /api/posts/123/comments - List post's comments
// POST /api/posts/123/comments - Add comment to post
// ✅ Nested resources
// ✅ Verify parent exists
// ✅ Filter by parent ID
// ✅ RESTful nestingMultiple Nested Params
// GET - Get specific post by specific user
export async function GET(
request: Request,
{ params }: { params: { userId: string; postId: string } }
) {
// Access multiple params
const { userId, postId } = params;
const post = await db.posts.findFirst({
where: {
id: postId,
authorId: userId, // Verify post belongs to user
},
include: {
author: { select: { id: true, name: true } },
},
});
if (!post) {
return Response.json(
{ error: 'Post not found or does not belong to this user' },
{ status: 404 }
);
}
return Response.json(post);
}
// DELETE - Delete user's specific post
export async function DELETE(
request: Request,
{ params }: { params: { userId: string; postId: string } }
) {
try {
// Delete only if belongs to user
const deleted = await db.posts.deleteMany({
where: {
id: params.postId,
authorId: params.userId,
},
});
if (deleted.count === 0) {
return Response.json(
{ error: 'Post not found or does not belong to user' },
{ status: 404 }
);
}
return new Response(null, { status: 204 });
} catch (error) {
return Response.json(
{ error: 'Failed to delete post' },
{ status: 500 }
);
}
}
// GET /api/users/user-123/posts/post-456
// DELETE /api/users/user-123/posts/post-456
// ✅ Multiple route params
// ✅ Verify ownership
// ✅ Scoped to parent resourceRESTful API Patterns
RESTful Route Structure
Standard REST Patterns
GET /api/posts- List posts (collection)POST /api/posts- Create post (collection)GET /api/posts/[id]- Get single post (resource)PUT /api/posts/[id]- Full update (resource)PATCH /api/posts/[id]- Partial update (resource)DELETE /api/posts/[id]- Delete (resource)
Nested Resources
GET /api/posts/[id]/comments- List post's commentsPOST /api/posts/[id]/comments- Create comment on postGET /api/users/[id]/posts- List user's posts
Resource Naming Conventions
// ✅ GOOD: Plural nouns for collections
/api/posts
/api/users
/api/products
/api/comments
// ✅ GOOD: Singular for single resource
/api/posts/[id]
/api/users/[userId]
// ❌ BAD: Verbs in URLs
/api/getPosts
/api/createUser
/api/deleteProduct
// ❌ BAD: Mixed singular/plural
/api/post // Should be /api/posts
/api/user/[id] // Should be /api/users/[id]
// Use HTTP methods for actions, not URLsQuery Parameters vs Route Parameters
// Route parameters - Identify specific resource
GET /api/posts/123 // params.id = '123'
GET /api/users/user-456 // params.userId = 'user-456'
// Query parameters - Filter, sort, paginate collection
GET /api/posts?category=tech&page=2
GET /api/users?role=admin&status=active
// ✅ GOOD: Route params for resource identity
export async function GET(
request: Request,
{ params }: { params: { id: string } }
) {
const post = await db.posts.findUnique({
where: { id: params.id }, // Route param
});
}
// ✅ GOOD: Query params for filtering
export async function GET(request: Request) {
const { searchParams } = new URL(request.url);
const category = searchParams.get('category'); // Query param
const posts = await db.posts.findMany({
where: { category },
});
}Advanced Dynamic Route Patterns
Slug-Based Routes
// GET post by slug instead of ID
export async function GET(
request: Request,
{ params }: { params: { slug: string } }
) {
const post = await db.posts.findUnique({
where: { slug: params.slug },
include: {
author: { select: { id: true, name: true } },
},
});
if (!post) {
return Response.json(
{ error: 'Post not found' },
{ status: 404 }
);
}
return Response.json(post);
}
// GET /api/posts/by-slug/my-awesome-post
// ✅ Human-readable URLs
// ✅ SEO-friendly
// ✅ Unique slug constraint in databaseCatch-All Routes
// Catch-all route for file paths
export async function GET(
request: Request,
{ params }: { params: { path: string[] } }
) {
// params.path is array of segments
const filepath = params.path.join('/');
console.log('Requested path:', filepath);
// /api/files/images/users/avatar.jpg
// params.path = ['images', 'users', 'avatar.jpg']
// Serve file or return info
const file = await getFile(filepath);
if (!file) {
return Response.json(
{ error: 'File not found' },
{ status: 404 }
);
}
return Response.json({
path: filepath,
size: file.size,
url: file.url,
});
}
// GET /api/files/images/users/avatar.jpg
// ✅ [...path] captures multiple segments
// ✅ params.path is array
// ✅ Flexible file routingOptional Catch-All Routes
// Optional catch-all - matches with or without segments
export async function GET(
request: Request,
{ params }: { params: { query?: string[] } }
) {
const query = params.query;
if (!query || query.length === 0) {
// GET /api/search
return Response.json({
message: 'Provide search query',
examples: ['/api/search/posts/nextjs', '/api/search/users/john'],
});
}
const [type, term] = query;
if (type === 'posts') {
const posts = await db.posts.findMany({
where: {
OR: [
{ title: { contains: term, mode: 'insensitive' } },
{ content: { contains: term, mode: 'insensitive' } },
],
},
});
return Response.json({ type, term, results: posts });
}
if (type === 'users') {
const users = await db.users.findMany({
where: { name: { contains: term, mode: 'insensitive' } },
});
return Response.json({ type, term, results: users });
}
return Response.json(
{ error: 'Invalid search type' },
{ status: 400 }
);
}
// GET /api/search - Works (empty query)
// GET /api/search/posts/nextjs - Works
// GET /api/search/users/john - Works
// ✅ [[...query]] is optional
// ✅ Flexible API structureDynamic API Routes Structure
RESTful organization with dynamic parameters
Select a file or folder to see details
Dynamic API Routes Best Practices
1. Validate Route Parameters
// ✅ GOOD: Validate param format
export async function GET(
request: Request,
{ params }: { params: { id: string } }
) {
const id = params.id;
// Validate UUID format
const uuidRegex = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
if (!uuidRegex.test(id)) {
return Response.json(
{ error: 'Invalid ID format' },
{ status: 400 }
);
}
// Proceed with query
}
// Or validate with Zod
import { z } from 'zod';
const paramsSchema = z.object({
id: z.string().uuid(),
});
export async function GET(
request: Request,
{ params }: { params: { id: string } }
) {
const result = paramsSchema.safeParse(params);
if (!result.success) {
return Response.json(
{ error: 'Invalid parameters' },
{ status: 400 }
);
}
// Use validated result.data.id
}2. Return 404 for Missing Resources
// ✅ GOOD: Explicit 404 handling
export async function GET(
request: Request,
{ params }: { params: { id: string } }
) {
const resource = await db.items.findUnique({
where: { id: params.id },
});
if (!resource) {
return Response.json(
{ error: 'Resource not found' },
{ status: 404 }
);
}
return Response.json(resource);
}
// ❌ BAD: Return null or 200 for missing resource
export async function GET(
request: Request,
{ params }: { params: { id: string } }
) {
const resource = await db.items.findUnique({
where: { id: params.id },
});
return Response.json(resource); // null with 200 - wrong!
}3. Use Appropriate HTTP Methods
// ✅ GOOD: Semantic HTTP methods
GET /api/posts/[id] // Retrieve
PUT /api/posts/[id] // Full update
PATCH /api/posts/[id] // Partial update
DELETE /api/posts/[id] // Delete
// ❌ BAD: POST for everything
POST /api/posts/[id]?action=get
POST /api/posts/[id]?action=update
POST /api/posts/[id]?action=delete
// Use proper HTTP methods4. Verify Resource Ownership
// ✅ GOOD: Check authorization
export async function DELETE(
request: Request,
{ params }: { params: { id: string } }
) {
const userId = 'current-user-id'; // From auth
const post = await db.posts.findFirst({
where: {
id: params.id,
authorId: userId, // Verify ownership
},
});
if (!post) {
return Response.json(
{ error: 'Post not found or unauthorized' },
{ status: 404 } // Don't leak existence
);
}
await db.posts.delete({ where: { id: params.id } });
return new Response(null, { status: 204 });
}
// Protect resources from unauthorized access5. Keep Nesting Shallow
// ✅ GOOD: Shallow nesting (1-2 levels)
/api/posts/[postId]/comments
/api/users/[userId]/posts
// ⚠️ ACCEPTABLE: 3 levels if necessary
/api/organizations/[orgId]/teams/[teamId]/members
// ❌ BAD: Deep nesting
/api/orgs/[orgId]/depts/[deptId]/teams/[teamId]/members/[memberId]/tasks
// Keep URLs readable and maintainableKey Takeaways
- [id] in folder name - creates dynamic segment
- Collection + Resource - /posts and /posts/[id]
- GET, PUT, PATCH, DELETE - single resource methods
- Nested resources - /posts/[id]/comments
- Return 404 - for missing resources
- Validate params - check format and existence
- RESTful patterns - plural nouns, HTTP methods
What's Next?
You've mastered dynamic API routes! Next, we'll explore Request and Response Objects—working with headers, cookies, setting custom headers, handling different content types, and using NextRequest/NextResponse for advanced features. You'll gain complete control over HTTP communication!
We'll cover request headers, response headers, cookies, redirects, rewrites, and streaming responses.
🔐 Security First
Always verify resource ownership before allowing modifications, validate all route parameters, use proper authentication, don't leak resource existence in error messages, and implement rate limiting for public APIs. Security is critical for production APIs!