Next.js Route Handlers: REST API Tasarımı

Next.js Route Handlers: REST API Tasarımı
Next.js App Router'daki Route Handlers, sunucu tarafında API endpoint'leri oluşturmanızı sağlar. Web Request ve Response API'lerini kullanarak RESTful API'ler tasarlayabilirsiniz. Bu yazıda Route Handler'ların çalışma prensibini, CRUD operasyonlarını, middleware pattern'lerini ve production-ready API tasarım stratejilerini inceleyeceğiz.
Route Handler Temelleri
Route Handler'lar, route.js (veya route.ts) dosyalarında HTTP method'larına karşılık gelen fonksiyonlar olarak tanımlanır:
// app/api/hello/route.js
export async function GET(request) {
return Response.json({ message: 'Merhaba Dünya!' });
}
export async function POST(request) {
const body = await request.json();
return Response.json({ received: body }, { status: 201 });
}
// Desteklenen HTTP method'ları:
// GET, POST, PUT, PATCH, DELETE, HEAD, OPTIONS
Dinamik Route'lar
// app/api/users/[id]/route.js
export async function GET(request, { params }) {
const { id } = await params;
const user = await db.user.findUnique({ where: { id } });
if (!user) {
return Response.json(
{ error: 'Kullanıcı bulunamadı' },
{ status: 404 }
);
}
return Response.json(user);
}
export async function PUT(request, { params }) {
const { id } = await params;
const body = await request.json();
const user = await db.user.update({
where: { id },
data: body,
});
return Response.json(user);
}
export async function DELETE(request, { params }) {
const { id } = await params;
await db.user.delete({ where: { id } });
return new Response(null, { status: 204 });
}
Tam CRUD API Örneği
// app/api/posts/route.js
import { prisma } from '@/lib/prisma';
import { NextResponse } from 'next/server';
import { z } from 'zod';
const CreatePostSchema = z.object({
title: z.string().min(1).max(200),
content: z.string().min(1),
published: z.boolean().default(false),
categoryId: z.string().optional(),
});
// GET /api/posts — Tüm yazıları listele
export async function GET(request) {
const { searchParams } = new URL(request.url);
const page = parseInt(searchParams.get('page') || '1');
const limit = parseInt(searchParams.get('limit') || '10');
const search = searchParams.get('search') || '';
const category = searchParams.get('category');
const skip = (page - 1) * limit;
const where = {
published: true,
...(search && {
OR: [
{ title: { contains: search, mode: 'insensitive' } },
{ content: { contains: search, mode: 'insensitive' } },
],
}),
...(category && { categoryId: category }),
};
const [posts, total] = await Promise.all([
prisma.post.findMany({
where,
skip,
take: limit,
orderBy: { createdAt: 'desc' },
include: { author: { select: { name: true, image: true } } },
}),
prisma.post.count({ where }),
]);
return Response.json({
data: posts,
pagination: {
page,
limit,
total,
totalPages: Math.ceil(total / limit),
},
});
}
// POST /api/posts — Yeni yazı oluştur
export async function POST(request) {
try {
const body = await request.json();
const validated = CreatePostSchema.parse(body);
const post = await prisma.post.create({
data: {
...validated,
authorId: 'current-user-id', // Auth'dan alınmalı
slug: validated.title.toLowerCase().replace(/\s+/g, '-'),
},
});
return Response.json(post, { status: 201 });
} catch (error) {
if (error instanceof z.ZodError) {
return Response.json(
{ error: 'Validasyon hatası', details: error.errors },
{ status: 400 }
);
}
return Response.json(
{ error: 'Sunucu hatası' },
{ status: 500 }
);
}
}
Query Parameters ve Headers
export async function GET(request) {
// URL ve query parameters
const { searchParams } = new URL(request.url);
const sort = searchParams.get('sort') || 'createdAt';
const order = searchParams.get('order') || 'desc';
// Request headers
const authHeader = request.headers.get('Authorization');
const contentType = request.headers.get('Content-Type');
const userAgent = request.headers.get('User-Agent');
// Cookies
const token = request.cookies.get('session-token');
// Response headers
return Response.json(
{ data: [] },
{
headers: {
'Cache-Control': 'public, max-age=60, s-maxage=300',
'X-Total-Count': '100',
'Access-Control-Allow-Origin': '*',
},
}
);
}
Middleware Pattern
// lib/api-middleware.js
// Authentication middleware
export function withAuth(handler) {
return async (request, context) => {
const token = request.headers.get('Authorization')?.replace('Bearer ', '');
if (!token) {
return Response.json({ error: 'Unauthorized' }, { status: 401 });
}
try {
const user = await verifyToken(token);
request.user = user;
return handler(request, context);
} catch {
return Response.json({ error: 'Invalid token' }, { status: 401 });
}
};
}
// Rate limiting middleware
const rateLimit = new Map();
export function withRateLimit(handler, { limit = 60, window = 60000 } = {}) {
return async (request, context) => {
const ip = request.headers.get('x-forwarded-for') || 'unknown';
const now = Date.now();
const record = rateLimit.get(ip) || { count: 0, resetAt: now + window };
if (now > record.resetAt) {
record.count = 0;
record.resetAt = now + window;
}
record.count++;
rateLimit.set(ip, record);
if (record.count > limit) {
return Response.json(
{ error: 'Rate limit exceeded' },
{
status: 429,
headers: { 'Retry-After': String(Math.ceil((record.resetAt - now) / 1000)) },
}
);
}
return handler(request, context);
};
}
// Kullanım
// app/api/protected/route.js
import { withAuth, withRateLimit } from '@/lib/api-middleware';
async function handler(request) {
return Response.json({ user: request.user });
}
export const GET = withAuth(withRateLimit(handler));
Dosya Yükleme
// app/api/upload/route.js
import { writeFile } from 'fs/promises';
import path from 'path';
export async function POST(request) {
const formData = await request.formData();
const file = formData.get('file');
if (!file) {
return Response.json({ error: 'Dosya gerekli' }, { status: 400 });
}
const bytes = await file.arrayBuffer();
const buffer = Buffer.from(bytes);
const fileName = `${Date.now()}-${file.name}`;
const uploadDir = path.join(process.cwd(), 'public/uploads');
await writeFile(path.join(uploadDir, fileName), buffer);
return Response.json({
url: `/uploads/${fileName}`,
name: file.name,
size: file.size,
});
}
Streaming Response
// app/api/stream/route.js
export async function GET() {
const encoder = new TextEncoder();
const stream = new ReadableStream({
async start(controller) {
for (let i = 0; i < 10; i++) {
const chunk = encoder.encode(`data: Mesaj ${i + 1}\n\n`);
controller.enqueue(chunk);
await new Promise((resolve) => setTimeout(resolve, 1000));
}
controller.close();
},
});
return new Response(stream, {
headers: {
'Content-Type': 'text/event-stream',
'Cache-Control': 'no-cache',
Connection: 'keep-alive',
},
});
}
CORS Yapılandırması
// app/api/public/route.js
const corsHeaders = {
'Access-Control-Allow-Origin': 'https://trusted-site.com',
'Access-Control-Allow-Methods': 'GET, POST, PUT, DELETE, OPTIONS',
'Access-Control-Allow-Headers': 'Content-Type, Authorization',
'Access-Control-Max-Age': '86400',
};
export async function OPTIONS() {
return new Response(null, { headers: corsHeaders });
}
export async function GET(request) {
const data = await fetchData();
return Response.json(data, { headers: corsHeaders });
}
Hata Yönetimi
// lib/api-errors.js
export class ApiError extends Error {
constructor(message, status = 500) {
super(message);
this.status = status;
}
}
export function handleApiError(error) {
console.error('API Error:', error);
if (error instanceof ApiError) {
return Response.json({ error: error.message }, { status: error.status });
}
if (error instanceof z.ZodError) {
return Response.json(
{ error: 'Validasyon hatası', details: error.flatten() },
{ status: 400 }
);
}
return Response.json({ error: 'Internal Server Error' }, { status: 500 });
}
request nesnesini kullanın, cookies() veya headers() çağırın veya GET dışındaki HTTP method'larını kullanın.
route.js ve page.js aynı route segment'ında birlikte bulunamaz. API endpoint'lerinizi /api prefix'i altında tutarak bu çakışmayı önleyin.
Sonuç
Next.js Route Handlers, Web Standard API'leri kullanarak modern REST API'ler oluşturmanın güçlü bir yoludur. Dinamik route'lar, query parameter işleme, validasyon, middleware pattern'leri, dosya yükleme, streaming response ve CORS yapılandırması ile tam özellikli bir API katmanı oluşturabilirsiniz. Server Actions'ın form mutasyonları için daha uygun olduğu durumları göz önünde bulundurarak, harici tüketim (mobil uygulama, 3. parti entegrasyon) gerektiren API'ler için Route Handlers'ı tercih edin.