GET and POST are the most common HTTP methods. GET retrieves data using query parameters for filtering, while POST creates resources by sending data in the request body. Master these patterns and you'll build RESTful APIs that follow web standards, handle edge cases gracefully, and provide excellent developer experience. Let's build production-ready GET and POST handlers!
GET Request Handlers
Basic GET Handler
export async function GET(request: Request) {
// Fetch all posts
const posts = await db.posts.findMany({
orderBy: { createdAt: 'desc' },
});
return Response.json(posts);
}
// GET /api/posts
// Returns: [{ id: 1, title: '...', ... }, ...]
// ✅ Fetches data from database
// ✅ Returns JSON array
// ✅ No query parameters needed for simple listGET with Query Parameters
export async function GET(request: Request) {
// Parse URL to get query parameters
const { searchParams } = new URL(request.url);
// Extract parameters
const page = parseInt(searchParams.get('page') || '1', 10);
const limit = parseInt(searchParams.get('limit') || '10', 10);
const category = searchParams.get('category');
const search = searchParams.get('search');
// Build query
const where: any = {};
if (category) {
where.category = category;
}
if (search) {
where.title = {
contains: search,
mode: 'insensitive',
};
}
// Fetch with pagination
const posts = await db.posts.findMany({
where,
skip: (page - 1) * limit,
take: limit,
orderBy: { createdAt: 'desc' },
});
// Get total count for pagination
const total = await db.posts.count({ where });
return Response.json({
posts,
pagination: {
page,
limit,
total,
totalPages: Math.ceil(total / limit),
},
});
}
// GET /api/posts?page=2&limit=20&category=tech&search=nextjs
// ✅ Pagination support
// ✅ Category filtering
// ✅ Search functionality
// ✅ Total count for pagination metadataGET with Multiple Query Parameters
export async function GET(request: Request) {
const { searchParams } = new URL(request.url);
// Multiple values for same parameter
// GET /api/search?tag=react&tag=nextjs&tag=typescript
const tags = searchParams.getAll('tag');
// Price range
const minPrice = searchParams.get('minPrice');
const maxPrice = searchParams.get('maxPrice');
// Sorting
const sortBy = searchParams.get('sortBy') || 'createdAt';
const sortOrder = searchParams.get('sortOrder') || 'desc';
// Build query
const where: any = {};
if (tags.length > 0) {
where.tags = {
hasSome: tags,
};
}
if (minPrice || maxPrice) {
where.price = {};
if (minPrice) where.price.gte = parseFloat(minPrice);
if (maxPrice) where.price.lte = parseFloat(maxPrice);
}
// Fetch products
const products = await db.products.findMany({
where,
orderBy: { [sortBy]: sortOrder },
});
return Response.json({
products,
filters: {
tags,
minPrice,
maxPrice,
sortBy,
sortOrder,
},
});
}
// GET /api/search?tag=react&tag=nextjs&minPrice=10&maxPrice=100&sortBy=price&sortOrder=asc
// ✅ Multiple values with getAll()
// ✅ Range filtering
// ✅ Dynamic sorting
// ✅ Return applied filtersGET with Conditional Response
export async function GET(request: Request) {
const { searchParams } = new URL(request.url);
const userId = searchParams.get('id');
if (!userId) {
return Response.json(
{ error: 'User ID is required' },
{ status: 400 }
);
}
const user = await db.users.findUnique({
where: { id: userId },
select: {
id: true,
name: true,
email: true,
// Don't expose password!
},
});
if (!user) {
return Response.json(
{ error: 'User not found' },
{ status: 404 }
);
}
return Response.json(user);
}
// GET /api/user?id=123
// ✅ Validate required parameters
// ✅ Return 404 if not found
// ✅ Return 400 for invalid input
// ✅ Select only safe fieldsPOST Request Handlers
Basic POST Handler
export async function POST(request: Request) {
// Parse JSON body
const body = await request.json();
// Create post
const post = await db.posts.create({
data: {
title: body.title,
content: body.content,
authorId: body.authorId,
},
});
// Return created resource with 201 status
return Response.json(post, { status: 201 });
}
// POST /api/posts
// Body: { "title": "My Post", "content": "...", "authorId": "123" }
// Returns: { "id": 1, "title": "My Post", ... } with status 201
// ✅ Parse request body
// ✅ Create resource
// ✅ Return 201 Created
// ✅ Include created resource in responsePOST with Validation
import { z } from 'zod';
// Define validation schema
const createPostSchema = z.object({
title: z.string().min(1, 'Title is required').max(200),
content: z.string().min(10, 'Content must be at least 10 characters'),
authorId: z.string().uuid('Invalid author ID'),
tags: z.array(z.string()).optional(),
published: z.boolean().default(false),
});
export async function POST(request: Request) {
// Parse body
const body = await request.json();
// Validate
const result = createPostSchema.safeParse(body);
if (!result.success) {
return Response.json(
{
error: 'Validation failed',
details: result.error.flatten().fieldErrors,
},
{ status: 400 }
);
}
// Type-safe validated data
const validatedData = result.data;
// Create post
const post = await db.posts.create({
data: validatedData,
});
return Response.json(post, { status: 201 });
}
// POST /api/posts
// Body: { "title": "", "content": "short" }
// Returns:
// {
// "error": "Validation failed",
// "details": {
// "title": ["Title is required"],
// "content": ["Content must be at least 10 characters"]
// }
// } with status 400
// ✅ Zod validation
// ✅ Type-safe data
// ✅ Detailed error messages
// ✅ Field-specific errorsPOST with Nested Data
import { z } from 'zod';
const createOrderSchema = z.object({
userId: z.string().uuid(),
items: z.array(z.object({
productId: z.string().uuid(),
quantity: z.number().int().positive(),
price: z.number().positive(),
})).min(1, 'At least one item required'),
shippingAddress: z.object({
street: z.string(),
city: z.string(),
zipCode: z.string(),
country: z.string(),
}),
paymentMethod: z.enum(['credit_card', 'paypal', 'bank_transfer']),
});
export async function POST(request: Request) {
const body = await request.json();
const result = createOrderSchema.safeParse(body);
if (!result.success) {
return Response.json(
{ error: 'Validation failed', details: result.error.flatten() },
{ status: 400 }
);
}
const data = result.data;
// Calculate total
const total = data.items.reduce(
(sum, item) => sum + item.price * item.quantity,
0
);
// Create order with nested relations
const order = await db.orders.create({
data: {
userId: data.userId,
total,
status: 'pending',
paymentMethod: data.paymentMethod,
shippingAddress: {
create: data.shippingAddress,
},
items: {
create: data.items,
},
},
include: {
items: true,
shippingAddress: true,
},
});
return Response.json(order, { status: 201 });
}
// POST /api/orders
// Body: {
// "userId": "...",
// "items": [{ "productId": "...", "quantity": 2, "price": 29.99 }],
// "shippingAddress": { "street": "...", ... },
// "paymentMethod": "credit_card"
// }
// ✅ Nested data validation
// ✅ Complex schema
// ✅ Nested database creation
// ✅ Include related dataPOST with File Upload
import { writeFile } from 'fs/promises';
import { join } from 'path';
export async function POST(request: Request) {
// Parse form data
const formData = await request.formData();
const file = formData.get('file') as File;
const title = formData.get('title') as string;
const description = formData.get('description') as string;
if (!file) {
return Response.json(
{ error: 'No file uploaded' },
{ status: 400 }
);
}
// Validate file type
const allowedTypes = ['image/jpeg', 'image/png', 'image/webp'];
if (!allowedTypes.includes(file.type)) {
return Response.json(
{ error: 'Invalid file type. Only JPEG, PNG, and WebP allowed' },
{ status: 400 }
);
}
// Validate file size (5MB max)
const maxSize = 5 * 1024 * 1024;
if (file.size > maxSize) {
return Response.json(
{ error: 'File too large. Maximum size is 5MB' },
{ status: 400 }
);
}
// Convert to buffer
const bytes = await file.arrayBuffer();
const buffer = Buffer.from(bytes);
// Generate unique filename
const filename = `${Date.now()}-${file.name}`;
const filepath = join(process.cwd(), 'public', 'uploads', filename);
// Save file
await writeFile(filepath, buffer);
// Save metadata to database
const upload = await db.uploads.create({
data: {
filename,
originalName: file.name,
mimeType: file.type,
size: file.size,
title,
description,
url: `/uploads/${filename}`,
},
});
return Response.json(upload, { status: 201 });
}
// POST /api/upload (multipart/form-data)
// FormData:
// file: [File object]
// title: "My Image"
// description: "Description"
// ✅ FormData parsing
// ✅ File type validation
// ✅ File size validation
// ✅ Save to disk
// ✅ Store metadata in databaseCombining GET and POST
Complete CRUD Endpoint
import { z } from 'zod';
import { NextResponse } from 'next/server';
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 with filtering
export async function GET(request: Request) {
const { searchParams } = new URL(request.url);
const page = parseInt(searchParams.get('page') || '1', 10);
const limit = parseInt(searchParams.get('limit') || '10', 10);
const published = searchParams.get('published');
const search = searchParams.get('search');
// Build where clause
const where: any = {};
if (published !== null) {
where.published = published === 'true';
}
if (search) {
where.OR = [
{ title: { contains: search, mode: 'insensitive' } },
{ content: { contains: search, mode: 'insensitive' } },
];
}
try {
const [posts, total] = await Promise.all([
db.posts.findMany({
where,
skip: (page - 1) * limit,
take: limit,
orderBy: { createdAt: 'desc' },
include: {
author: {
select: { id: true, name: true },
},
},
}),
db.posts.count({ where }),
]);
return NextResponse.json({
posts,
pagination: {
page,
limit,
total,
totalPages: Math.ceil(total / limit),
},
});
} catch (error) {
console.error('Failed to fetch posts:', error);
return NextResponse.json(
{ error: 'Failed to fetch posts' },
{ status: 500 }
);
}
}
// POST - Create new post
export async function POST(request: Request) {
try {
const body = await request.json();
const result = createPostSchema.safeParse(body);
if (!result.success) {
return NextResponse.json(
{
error: 'Validation failed',
details: result.error.flatten().fieldErrors,
},
{ status: 400 }
);
}
const post = await db.posts.create({
data: {
...result.data,
authorId: 'user-id', // In production, get from auth
},
include: {
author: {
select: { id: true, name: true },
},
},
});
return NextResponse.json(post, { status: 201 });
} catch (error) {
console.error('Failed to create post:', error);
return NextResponse.json(
{ error: 'Failed to create post' },
{ status: 500 }
);
}
}
// ✅ GET with pagination, filtering, search
// ✅ POST with validation
// ✅ Error handling for both
// ✅ Type-safe responses
// ✅ Include related dataResponse Patterns
Success Response Pattern
// Pattern 1: Simple success
export async function POST(request: Request) {
const post = await createPost(data);
return Response.json(post, { status: 201 });
}
// Pattern 2: Success with metadata
export async function GET(request: Request) {
const posts = await getPosts();
return Response.json({
success: true,
data: posts,
timestamp: new Date().toISOString(),
});
}
// Pattern 3: Success with pagination
export async function GET(request: Request) {
return Response.json({
data: posts,
pagination: {
page: 1,
limit: 10,
total: 100,
totalPages: 10,
},
});
}
// Choose pattern based on API needsError Response Pattern
// Pattern 1: Simple error
return Response.json(
{ error: 'Resource not found' },
{ status: 404 }
);
// Pattern 2: Error with details
return Response.json(
{
error: 'Validation failed',
message: 'The provided data is invalid',
details: {
title: ['Title is required'],
email: ['Invalid email format'],
},
},
{ status: 400 }
);
// Pattern 3: Error with code
return Response.json(
{
error: {
code: 'INVALID_CREDENTIALS',
message: 'Invalid email or password',
timestamp: new Date().toISOString(),
},
},
{ status: 401 }
);
// Consistent error format helps clientsCORS Headers
export async function GET(request: Request) {
const data = { message: 'Public API' };
return new Response(JSON.stringify(data), {
status: 200,
headers: {
'Content-Type': 'application/json',
'Access-Control-Allow-Origin': '*',
'Access-Control-Allow-Methods': 'GET, POST, PUT, DELETE, OPTIONS',
'Access-Control-Allow-Headers': 'Content-Type, Authorization',
},
});
}
// Handle preflight OPTIONS request
export async function OPTIONS(request: Request) {
return new Response(null, {
status: 204,
headers: {
'Access-Control-Allow-Origin': '*',
'Access-Control-Allow-Methods': 'GET, POST, PUT, DELETE, OPTIONS',
'Access-Control-Allow-Headers': 'Content-Type, Authorization',
},
});
}
// ✅ CORS for public APIs
// ✅ OPTIONS for preflight
// ✅ Allow specific origins in productionGET and POST API Structure
Organization of GET and POST handlers
Select a file or folder to see details
GET and POST Best Practices
1. Validate All Input
// ✅ GOOD: Validate with Zod
import { z } from 'zod';
const schema = z.object({
email: z.string().email(),
age: z.number().int().min(18),
});
export async function POST(request: Request) {
const body = await request.json();
const result = schema.safeParse(body);
if (!result.success) {
return Response.json(
{ errors: result.error.flatten() },
{ status: 400 }
);
}
// Use validated result.data
}
// ❌ BAD: No validation
export async function POST(request: Request) {
const body = await request.json();
// Directly use body.email, body.age - unsafe!
}2. Use Proper Status Codes
// ✅ GOOD: Meaningful status codes
// 200 - GET success
return Response.json(posts);
// 201 - POST created resource
return Response.json(post, { status: 201 });
// 400 - Bad request (validation error)
return Response.json({ error: 'Invalid data' }, { status: 400 });
// 404 - Not found
return Response.json({ error: 'Not found' }, { status: 404 });
// 500 - Server error
return Response.json({ error: 'Server error' }, { status: 500 });
// Status codes communicate meaning to clients3. Paginate Large Datasets
// ✅ GOOD: Always paginate
export async function GET(request: Request) {
const { searchParams } = new URL(request.url);
const page = parseInt(searchParams.get('page') || '1');
const limit = Math.min(parseInt(searchParams.get('limit') || '10'), 100);
const items = await db.items.findMany({
skip: (page - 1) * limit,
take: limit,
});
return Response.json({
items,
pagination: { page, limit },
});
}
// ❌ BAD: Return all items
export async function GET(request: Request) {
const items = await db.items.findMany(); // Could be millions!
return Response.json(items);
}
// Always limit response size4. Handle Errors Gracefully
// ✅ GOOD: Try-catch with proper errors
export async function GET(request: Request) {
try {
const data = await db.items.findMany();
return Response.json(data);
} catch (error) {
console.error('Database error:', error);
return Response.json(
{ error: 'Failed to fetch items' },
{ status: 500 }
);
}
}
// Don't expose internal error details to clients5. Sanitize and Escape Input
// ✅ GOOD: Sanitize input
export async function POST(request: Request) {
const body = await request.json();
// Trim whitespace
const title = body.title?.trim();
// Escape HTML to prevent XSS
const sanitizedContent = escapeHtml(body.content);
// Validate after sanitization
const result = schema.safeParse({
title,
content: sanitizedContent,
});
// Use validated data
}
// Protect against injection attacksKey Takeaways
- GET with searchParams - filter and paginate data
- POST with request.json() - parse request body
- 201 for POST - return created resource
- Validate with Zod - type-safe validation
- Always paginate - limit response sizes
- Proper status codes - 200, 201, 400, 404, 500
- Error handling - try-catch and graceful failures
- FormData for files - multipart uploads
What's Next?
You've mastered GET and POST handlers! Next, we'll explore Dynamic API Routes—creating RESTful endpoints with route parameters, building resource-specific handlers like /api/posts/[id], and implementing complete CRUD operations with dynamic segments. You'll build flexible, scalable APIs!
We'll cover dynamic route parameters, RESTful patterns, single resource endpoints, and complete CRUD implementations.
📊 Performance Matters
Always paginate large datasets, add database indexes for frequently queried fields, cache responses when appropriate, and use SELECT to fetch only needed fields. Fast APIs provide better user experience!