İçeriğe geç
nextjs

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

Tarık Tunç
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 });
}
İpucu: Route Handler'lar, varsayılan olarak GET istekleri için statik olarak cache'lenir. Dinamik davranış istiyorsanız request nesnesini kullanın, cookies() veya headers() çağırın veya GET dışındaki HTTP method'larını kullanın.
Uyarı: 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.

Kaynaklar

İlgili Yazılar