11. Backend Development
Route Handlers
In Next.js, API routes are defined under the app/api directory. Each route file exports an async function corresponding to an HTTP method. The request object is of type NextRequest, providing access to URL, headers, cookies, and body. Below we explore each method with detailed examples and best practices.
GET Requests
The GET handler retrieves resources, often with query parameters for filtering, pagination, or sorting. It should be idempotent and safe.
export async function GET(request: NextRequest) {
const searchParams = request.nextUrl.searchParams;
const limit = searchParams.get('limit') ?? '10';
const category = searchParams.get('category');
let query = 'SELECT * FROM products';
const params: any[] = [];
if (category) {
query += ' WHERE category = $' + (params.length + 1);
params.push(category);
}
query += ' LIMIT $' + (params.length + 1);
params.push(limit);
const data = await db.query(query, params);
return NextResponse.json(data);
}
Example: GET /api/products?limit=10&category=electronics returns the first ten electronics products. To enable caching for static data, export const dynamic = 'force-static' or set revalidate = 60 for incremental static regeneration.
POST Requests
POST creates a new resource. The handler must validate the incoming payload, persist it, and return the created entity with a 201 Created status and a Location header pointing to the new resource.
export async function POST(request: NextRequest) {
const body = await request.json();
const validated = schema.parse(body); // Zod validation
const result = await db.insert(validated);
const url = request.nextUrl.origin + `/api/products/${result.id}`;
return NextResponse.json(result, { status: 201, headers: { Location: url } });
}
Example: POST /api/orders with body { userId: '123', items: [{ productId: '456', qty: 2 }] } creates an order and returns 201 with Location: /api/orders/789.
PUT Requests
PUT replaces an existing resource entirely. It is idempotent; repeating the same request yields the same state. The handler receives the resource ID via route parameters.
export async function PUT(request: NextRequest, { params }: { params: { id: string } }) {
const body = await request.json();
const updated = await db.update(params.id, body); // full replacement
return NextResponse.json(updated);
}
Example: PUT /api/users/123 with a complete user object replaces the user record. If the resource does not exist, return 404 Not Found.
PATCH Requests
PATCH applies partial modifications. Unlike PUT, it is not required to be idempotent, especially when the update depends on current values (e.g., incrementing a counter).
export async function PATCH(request: NextRequest, { params }: { params: { id: string } }) {
const body = await request.json();
const updated = await db.patch(params.id, body); // merges fields
return NextResponse.json(updated);
}
Example: PATCH /api/users/123 with { name: 'New Name' } updates only the name field.
DELETE Requests
DELETE removes a resource. Successful deletion returns 204 No Content with an empty body. For soft deletes, update a deletedAt timestamp instead of removing the row.
export async function DELETE(request: NextRequest, { params }: { params: { id: string } }) {
await db.delete(params.id);
return new NextResponse(null, { status: 204 });
}
Example: DELETE /api/posts/456 removes the post. Soft‑delete alternative:
await db.update(params.id, { deletedAt: new Date() });
API Design
Following RESTful conventions ensures predictable, scalable APIs. This section outlines URL design, HTTP method mapping, status codes, versioning, and pagination.
REST Principles
- Resource‑based URLs: Use nouns, not verbs. Collections:
/api/users, individual items:/api/users/:id, sub‑resources:/api/users/:id/posts. - HTTP methods map to CRUD:
- GET → Read
- POST → Create
- PUT → Replace (full update)
- PATCH → Modify (partial update)
- DELETE → Remove
- Status codes: Use the appropriate code for each outcome.
| Code | Meaning | Typical Use |
|---|---|---|
| 200 | OK | Successful GET, PUT, PATCH |
| 201 | Created | Successful POST |
| 204 | No Content | Successful DELETE |
| 400 | Bad Request | Validation error |
| 401 | Unauthorized | Missing or invalid auth |
| 403 | Forbidden | Authenticated but insufficient rights |
| 404 | Not Found | Resource does not exist |
| 500 | Internal Server Error | Unexpected server failure |
Example: GET /api/users/123/posts?page=2&limit=20 returns the second page of a user's posts, twenty per page.
Versioning
API evolution should not break existing clients. Two common strategies:
- URL versioning:
/api/v1/users,/api/v2/users. - Header versioning:
Accept: application/vnd.api.v1+json.
Validation
Input validation protects against malformed data and injection attacks. Zod is a popular schema‑based validator that integrates seamlessly with Next.js API routes.
const schema = z.object({
email: z.string().email(),
password: z.string().min(8),
confirmPassword: z.string().min(8)
}).refine((data) => data.password