Next.js App Router'da Route Handlers: API Tasarımı, Webhook ve Streaming

Next.js 16 App Router'da Route Handlers ile API tasarımı, Stripe webhook entegrasyonu, SSE streaming, rate limiting ve CORS yapılandırmasını üretim kalitesinde kod örnekleriyle öğrenin.

Giriş

Serimizin altıncı bölümüne hoş geldiniz! Şimdiye kadar React Server Components ile veri çekme, Server Actions ile güvenli form işlemleri, Middleware ile Edge Runtime kalıpları, Paralel Rotalar ve Intercepting Routes ve önbellekleme ile revalidation stratejilerini derinlemesine incelemiştik. Şimdi sıra Next.js App Router'ın dış dünyaya açılan kapısında: Route Handlers.

Şöyle düşünün — Server Actions'ı form gönderimleri ve dahili mutasyonlar için kullanıyorsunuz, süper. Ama ya bir mobil uygulama API'nizle konuşmak isterse? Ya da Stripe bir ödeme bildirimi gönderirse? Ya da kullanıcılarınıza gerçek zamanlı bildirim akışı sağlamanız gerekirse? İşte tam bu noktada Route Handlers sahneye çıkıyor.

Bu rehberde Next.js 16'daki Route Handlers'ı sıfırdan ele alacağız. HTTP metotlarından dinamik parametrelere, webhook imza doğrulamasından SSE ile streaming yanıtlara, rate limiting'den CORS yapılandırmasına kadar üretim ortamında gerçekten ihtiyacınız olan her şeyi kod örnekleriyle birlikte göreceğiz.

Route Handlers Nedir?

Route Handlers, Next.js App Router'da Web Request ve Response API'lerini kullanarak belirli bir rota için özel HTTP istek işleyicileri oluşturmanızı sağlar. Pages Router'daki pages/api dizinindeki API Routes'un modern karşılığı diyebiliriz — ama çok daha güçlü ve esnek.

Temel yapı aslında oldukça basit: app dizini altında istediğiniz bir klasörde route.ts dosyası oluşturuyorsunuz ve HTTP metotlarına karşılık gelen fonksiyonları export ediyorsunuz.

// app/api/hello/route.ts
export async function GET() {
  return Response.json({ message: 'Merhaba Dünya!' })
}

export async function POST(request: Request) {
  const body = await request.json()
  return Response.json({ received: body }, { status: 201 })
}

Burada dikkat edilmesi gereken kritik bir nokta var: aynı rota segmentinde hem route.ts hem de page.tsx dosyası bulunamaz. Yani app/api/hello/ klasöründe bir route.ts varsa, aynı yere page.tsx ekleyemezsiniz. Bu ilk başta garip gelebilir ama mantıklı — aynı URL hem sayfa hem API endpoint olamaz.

Desteklenen HTTP Metotları

Route Handlers şu HTTP metotlarını destekler: GET, POST, PUT, PATCH, DELETE, HEAD ve OPTIONS. Desteklenmeyen bir metot çağrıldığında Next.js otomatik olarak 405 Method Not Allowed döndürür. OPTIONS metodunu tanımlamazsanız bile Next.js bunu sizin yerinize otomatik uygular — güzel bir dokunuş.

Route Handlers ve Server Actions: Ne Zaman Hangisi?

Bu muhtemelen en çok sorulan sorulardan biri. İkisi de sunucuda çalışıyor, ikisi de veri işleyebiliyor. Peki aralarındaki fark tam olarak ne?

Kısa Kural

Eğer yalnızca kendi Next.js uygulamanız çağırıyorsa ve veri mutasyonu yapıyorsanız → Server Actions. Dış dünyadan erişim gerekiyorsa veya HTTP yanıtı üzerinde tam kontrol istiyorsanız → Route Handlers. Bu kadar basit.

Detaylı Karşılaştırma

ÖzellikServer ActionsRoute Handlers
Temel kullanımForm işlemleri, dahili mutasyonlarPublic API, webhook, dış entegrasyon
HTTP metoduYalnızca POSTGET, POST, PUT, PATCH, DELETE, HEAD, OPTIONS
Dış erişimHayır — önceden tanımlanmış endpoint URL'i yokEvet — standart HTTP endpoint
Önbellek desteğiHayırEvet (GET metodu ile)
Yanıt kontrolüSınırlı — status code ve header ayarlanamazTam kontrol — status, header, streaming
React entegrasyonuGüçlü — useActionState, useFormStatus ileManuel — fetch ile çağrılır
StreamingHayırEvet — ReadableStream ile SSE desteği

Birkaç somut örnekle netleştirelim:

  • Kullanıcı profil formu güncelleme → Server Action (dahili mutasyon)
  • Stripe ödeme webhook'u → Route Handler (dış servis çağrısı)
  • Mobil uygulama için REST API → Route Handler (dış istemci)
  • Canlı bildirim akışı → Route Handler (SSE streaming)
  • Sepete ürün ekleme butonu → Server Action (dahili mutasyon)

NextRequest, NextResponse ve Dinamik Parametreler

Next.js, standart Web API'lerinin (Request ve Response) yanı sıra NextRequest ve NextResponse genişletmeleri sunuyor. Cookie yönetimi, URL parse etme, yönlendirme gibi günlük işlemler için bunlar hayat kurtarıcı.

Query Parametreleri

// app/api/search/route.ts
import { type NextRequest } from 'next/server'

export function GET(request: NextRequest) {
  const searchParams = request.nextUrl.searchParams
  const query = searchParams.get('q')
  const page = searchParams.get('page') ?? '1'
  const limit = searchParams.get('limit') ?? '10'

  // /api/search?q=next.js&page=2&limit=20
  return Response.json({
    query,
    page: parseInt(page),
    limit: parseInt(limit)
  })
}

Dinamik Rota Parametreleri (Next.js 16)

Next.js 16'da önemli bir değişiklik var: dinamik parametreler artık async Promise olarak geliyor. Bu, streaming destekli mimarinin bir parçası. Eski sürümlerden geçiş yapıyorsanız bu farkı mutlaka bilmeniz lazım.

// app/api/users/[id]/route.ts
import { NextRequest, NextResponse } from 'next/server'

export async function GET(
  request: NextRequest,
  { params }: { params: Promise<{ id: string }> }
) {
  const { id } = await params

  // Veritabanından kullanıcıyı çek
  const user = await db.user.findUnique({ where: { id } })

  if (!user) {
    return NextResponse.json(
      { error: 'Kullanıcı bulunamadı' },
      { status: 404 }
    )
  }

  return NextResponse.json(user)
}

export async function PUT(
  request: NextRequest,
  { params }: { params: Promise<{ id: string }> }
) {
  const { id } = await params
  const body = await request.json()

  const updatedUser = await db.user.update({
    where: { id },
    data: body
  })

  return NextResponse.json(updatedUser)
}

export async function DELETE(
  _request: NextRequest,
  { params }: { params: Promise<{ id: string }> }
) {
  const { id } = await params
  await db.user.delete({ where: { id } })

  return new Response(null, { status: 204 })
}

Next.js 16 ayrıca RouteContext yardımcı tipi sunuyor. Bu, parametreleri rota yapısına göre otomatik olarak tip-güvenli hâle getiriyor — TypeScript kullanıyorsanız bunu seveceksiniz:

// RouteContext ile tip güvenliği
import type { NextRequest } from 'next/server'

export async function GET(
  _req: NextRequest,
  ctx: RouteContext<'/users/[id]'>
) {
  const { id } = await ctx.params
  return Response.json({ id })
}

Cookie ve Header Yönetimi

// app/api/auth/session/route.ts
import { cookies, headers } from 'next/headers'
import { NextRequest, NextResponse } from 'next/server'

export async function GET(request: NextRequest) {
  // next/headers ile cookie okuma (async)
  const cookieStore = await cookies()
  const token = cookieStore.get('session-token')

  // Veya doğrudan NextRequest üzerinden
  const tokenAlt = request.cookies.get('session-token')

  // Header okuma
  const headerList = await headers()
  const userAgent = headerList.get('user-agent')

  if (!token) {
    return NextResponse.json(
      { error: 'Oturum bulunamadı' },
      { status: 401 }
    )
  }

  // Yanıta cookie ekleme
  const response = NextResponse.json({ authenticated: true })
  response.cookies.set('last-activity', new Date().toISOString(), {
    httpOnly: true,
    secure: true,
    sameSite: 'lax',
    maxAge: 60 * 60 * 24 // 1 gün
  })

  return response
}

Üretim Ortamında Webhook Entegrasyonu

Webhook'lar, Route Handlers'ın belki de en kritik kullanım alanı. Stripe ödeme bildirimleri, GitHub CI/CD tetikleyicileri, Twilio SMS bildirimleri — hepsi webhook'lar üzerinden çalışıyor. Ve açıkçası burada güvenlik her şeyden önce gelir, şaka değil.

Stripe Webhook Örneği (Üretim Kalitesinde)

// app/api/webhooks/stripe/route.ts
import Stripe from 'stripe'
import { NextRequest, NextResponse } from 'next/server'

const stripe = new Stripe(process.env.STRIPE_SECRET_KEY!)

export async function POST(request: NextRequest) {
  // 1. Raw body oku — request.json() DEĞİL!
  const body = await request.text()

  // 2. İmza header'ını al
  const signature = request.headers.get('stripe-signature')

  if (!signature) {
    return NextResponse.json(
      { error: 'İmza header''ı eksik' },
      { status: 400 }
    )
  }

  // 3. Webhook imzasını doğrula
  let event: Stripe.Event

  try {
    event = stripe.webhooks.constructEvent(
      body,
      signature,
      process.env.STRIPE_WEBHOOK_SECRET!
    )
  } catch (err) {
    console.error('Webhook imza doğrulama hatası:', err)
    return NextResponse.json(
      { error: 'Geçersiz imza' },
      { status: 400 }
    )
  }

  // 4. Event türüne göre işlem yap
  switch (event.type) {
    case 'checkout.session.completed': {
      const session = event.data.object as Stripe.Checkout.Session
      await handleCheckoutComplete(session)
      break
    }
    case 'invoice.payment_succeeded': {
      const invoice = event.data.object as Stripe.Invoice
      await handlePaymentSuccess(invoice)
      break
    }
    case 'customer.subscription.deleted': {
      const subscription = event.data.object as Stripe.Subscription
      await handleSubscriptionCanceled(subscription)
      break
    }
    default:
      console.log(`İşlenmeyen event türü: ${event.type}`)
  }

  // 5. Stripe'a başarılı yanıt dön
  return NextResponse.json({ received: true })
}

async function handleCheckoutComplete(
  session: Stripe.Checkout.Session
) {
  // Veritabanında siparişi güncelle
  await db.order.update({
    where: { stripeSessionId: session.id },
    data: {
      status: 'paid',
      paidAt: new Date()
    }
  })
}

async function handlePaymentSuccess(invoice: Stripe.Invoice) {
  // Abonelik süresini uzat
  await db.subscription.update({
    where: { stripeCustomerId: invoice.customer as string },
    data: {
      currentPeriodEnd: new Date(
        invoice.lines.data[0].period.end * 1000
      )
    }
  })
}

async function handleSubscriptionCanceled(
  subscription: Stripe.Subscription
) {
  // Kullanıcı aboneliğini iptal et
  await db.subscription.update({
    where: { stripeSubscriptionId: subscription.id },
    data: { status: 'canceled', canceledAt: new Date() }
  })
}

Kritik Noktalar

Bunları gerçekten aklınızda tutun, çünkü her biri ayrı bir baş ağrısı olabilir:

  • request.text() kullanın, request.json() değil: Stripe imza doğrulaması raw (ham) body üzerinden çalışır. JSON parse edilmiş body ile doğrulama her zaman başarısız olur. Bunu zor yoldan öğrenen çok geliştirici tanıyorum.
  • Webhook secret'ını NEXT_PUBLIC_ prefix'i olmadan saklayın: Bu prefix'li değişkenler istemci tarafına sızar. Webhook secret'ı yalnızca STRIPE_WEBHOOK_SECRET olarak .env.local dosyasında tutun.
  • Vercel'de Deployment Protection'ı webhook rotası için devre dışı bırakın: Aksi takdirde Stripe'ın webhook istekleri doğrudan reddedilir ve siz saatlerce neyin yanlış gittiğini ararsınız.
  • Middleware matcher'ınızı kontrol edin: Kimlik doğrulama middleware'iniz webhook rotasını engelleyebilir. Matcher'ı webhook rotalarını hariç tutacak şekilde ayarlamayı unutmayın.

SSE ile Gerçek Zamanlı Streaming Yanıtlar

Server-Sent Events (SSE), sunucudan istemciye tek yönlü ve sürekli veri akışı sağlayan hafif bir protokol. WebSocket'lerin aksine ek kütüphane gerektirmez — tarayıcılar EventSource API'si ile bunu zaten yerel olarak destekliyor. AI yanıt akışları, canlı bildirimler, dashboard güncellemeleri gibi senaryolarda gerçekten ideal bir çözüm.

Sunucu Tarafı: SSE Route Handler

// app/api/notifications/stream/route.ts
export const dynamic = 'force-dynamic'

export async function GET(request: Request) {
  const encoder = new TextEncoder()

  const stream = new ReadableStream({
    async start(controller) {
      // Bağlantı kapanma kontrolü
      const abortSignal = request.signal

      const sendEvent = (data: object, eventType = 'message') => {
        const payload =
          `event: ${eventType}\n` +
          `data: ${JSON.stringify(data)}\n` +
          `id: ${Date.now()}\n\n`
        controller.enqueue(encoder.encode(payload))
      }

      // İlk bağlantı bildirimi
      sendEvent({ status: 'connected' }, 'connection')

      // Gerçek zamanlı bildirim simülasyonu
      const interval = setInterval(() => {
        if (abortSignal.aborted) {
          clearInterval(interval)
          controller.close()
          return
        }

        sendEvent({
          id: crypto.randomUUID(),
          message: 'Yeni bildirim!',
          timestamp: new Date().toISOString()
        }, 'notification')
      }, 5000)

      // İstemci bağlantıyı kapattığında temizlik
      abortSignal.addEventListener('abort', () => {
        clearInterval(interval)
        controller.close()
      })
    }
  })

  return new Response(stream, {
    headers: {
      'Content-Type': 'text/event-stream; charset=utf-8',
      'Cache-Control': 'no-cache, no-transform',
      'Connection': 'keep-alive',
      'Content-Encoding': 'none',
      'X-Accel-Buffering': 'no' // NGINX tamponlamasını devre dışı bırak
    }
  })
}

İstemci Tarafı: EventSource ile Dinleme

// components/NotificationStream.tsx
'use client'

import { useEffect, useState } from 'react'

interface Notification {
  id: string
  message: string
  timestamp: string
}

export default function NotificationStream() {
  const [notifications, setNotifications] = useState<Notification[]>([])
  const [isConnected, setIsConnected] = useState(false)

  useEffect(() => {
    const eventSource = new EventSource('/api/notifications/stream')

    eventSource.addEventListener('connection', () => {
      setIsConnected(true)
    })

    eventSource.addEventListener('notification', (event) => {
      const data: Notification = JSON.parse(event.data)
      setNotifications((prev) => [data, ...prev].slice(0, 50))
    })

    eventSource.onerror = () => {
      setIsConnected(false)
      eventSource.close()
      // 3 saniye sonra yeniden bağlan
      setTimeout(() => {
        // Bileşen hâlâ mount ise yeniden bağlan
      }, 3000)
    }

    return () => {
      eventSource.close()
    }
  }, [])

  return (
    <div>
      <div>
        Durum: {isConnected ? 'Bağlı' : 'Bağlantı kesildi'}
      </div>
      <ul>
        {notifications.map((n) => (
          <li key={n.id}>{n.message} — {n.timestamp}</li>
        ))}
      </ul>
    </div>
  )
}

Altın Kural: Response'u Hemen Döndürün

SSE ile ilgili en yaygın hata şu: Route Handler fonksiyonunun tamamı bitmeden yanıt gönderilmiyor. Next.js, handler fonksiyonu tamamlanana kadar Response'u bekletebilir — ve bu, tüm chunk'ların tek seferde gelmesiyle sonuçlanır. Streaming'in amacına tamamen aykırı, değil mi?

Çözüm basit: ReadableStream ile Response'u hemen döndürün, async işlemleri stream'in start callback'i içinde çalıştırın.

Rate Limiting: API'nizi Kötüye Kullanıma Karşı Koruyun

Üretim ortamında API rate limiting olmazsa olmaz. Bunu atlayıp sonra pişman olan ekipleri gördüm. Harici paket kullanmadan, basit bir fixed window counter yaklaşımıyla uygulayabileceğiniz bir çözüme bakalım:

// lib/rate-limit.ts
const rateLimitMap = new Map<
  string,
  { count: number; lastReset: number }
>()

interface RateLimitConfig {
  windowMs: number  // Pencere süresi (ms)
  maxRequests: number // Pencere başına maksimum istek
}

export function rateLimit(
  ip: string,
  config: RateLimitConfig = { windowMs: 60_000, maxRequests: 10 }
): { success: boolean; remaining: number } {
  const now = Date.now()
  const record = rateLimitMap.get(ip)

  if (!record || now - record.lastReset > config.windowMs) {
    rateLimitMap.set(ip, { count: 1, lastReset: now })
    return { success: true, remaining: config.maxRequests - 1 }
  }

  if (record.count >= config.maxRequests) {
    return { success: false, remaining: 0 }
  }

  record.count++
  return {
    success: true,
    remaining: config.maxRequests - record.count
  }
}
// app/api/data/route.ts
import { NextRequest, NextResponse } from 'next/server'
import { rateLimit } from '@/lib/rate-limit'

export async function GET(request: NextRequest) {
  const ip =
    request.headers.get('x-forwarded-for')?.split(',')[0] ??
    'unknown'

  const { success, remaining } = rateLimit(ip, {
    windowMs: 60_000,  // 1 dakika
    maxRequests: 20     // Dakikada 20 istek
  })

  if (!success) {
    return NextResponse.json(
      { error: 'Çok fazla istek. Lütfen biraz bekleyin.' },
      {
        status: 429,
        headers: {
          'Retry-After': '60',
          'X-RateLimit-Remaining': '0'
        }
      }
    )
  }

  // Normal işlem
  const data = await fetchData()

  return NextResponse.json(data, {
    headers: {
      'X-RateLimit-Remaining': remaining.toString()
    }
  })
}

Önemli bir not: Bu yaklaşım tek bir sunucu instance'ı için çalışır. Birden fazla sunucu veya serverless ortamda (ki muhtemelen Vercel'de deploy ediyorsunuz) Redis tabanlı bir çözüm kullanmanız gerekir. @upstash/ratelimit bu iş için biçilmiş kaftan.

CORS Yapılandırması

Route Handlers varsayılan olarak yalnızca aynı origin'den gelen istekleri kabul eder. Farklı bir domain'den erişim gerekiyorsa — mesela mobil uygulama veya ayrı bir frontend — CORS header'larını yapılandırmanız şart.

Rota Bazlı CORS

// app/api/public/data/route.ts
const ALLOWED_ORIGINS = [
  'https://app.example.com',
  'https://mobile.example.com'
]

function getCorsHeaders(origin: string | null) {
  const headers: Record<string, string> = {
    'Access-Control-Allow-Methods': 'GET, POST, OPTIONS',
    'Access-Control-Allow-Headers': 'Content-Type, Authorization',
    'Access-Control-Max-Age': '86400' // 24 saat preflight cache
  }

  if (origin && ALLOWED_ORIGINS.includes(origin)) {
    headers['Access-Control-Allow-Origin'] = origin
  }

  return headers
}

export async function OPTIONS(request: NextRequest) {
  const origin = request.headers.get('origin')
  return new Response(null, {
    status: 204,
    headers: getCorsHeaders(origin)
  })
}

export async function GET(request: NextRequest) {
  const origin = request.headers.get('origin')
  const data = await fetchPublicData()

  return NextResponse.json(data, {
    headers: getCorsHeaders(origin)
  })
}

Uygulama Genelinde CORS: Middleware Kullanımı

Her Route Handler'a tek tek CORS eklemek yerine, tüm API rotaları için middleware kullanmak çok daha temiz bir yaklaşım. Bunu serimizin üçüncü bölümünde ele aldığımız Middleware katmanında kolayca uygulayabilirsiniz.

// middleware.ts
import { NextRequest, NextResponse } from 'next/server'

export function middleware(request: NextRequest) {
  // Yalnızca /api/public rotaları için CORS uygula
  if (request.nextUrl.pathname.startsWith('/api/public')) {
    const origin = request.headers.get('origin') ?? ''

    if (request.method === 'OPTIONS') {
      return new NextResponse(null, {
        status: 204,
        headers: {
          'Access-Control-Allow-Origin': origin,
          'Access-Control-Allow-Methods': 'GET, POST, PUT, DELETE',
          'Access-Control-Allow-Headers': 'Content-Type, Authorization',
          'Access-Control-Max-Age': '86400'
        }
      })
    }

    const response = NextResponse.next()
    response.headers.set('Access-Control-Allow-Origin', origin)
    return response
  }

  return NextResponse.next()
}

export const config = {
  matcher: ['/api/public/:path*']
}

Route Handler'larda Önbellek Davranışı

Serimizin beşinci bölümünde önbellekleme stratejilerini detaylıca incelemiştik. Route Handlers özelinde bilmeniz gereken temel kurallar şunlar:

  • Varsayılan olarak önbelleksiz: Next.js 15 ve sonrasında GET dahil tüm Route Handlers dinamik çalışır. Evet, varsayılan değişti.
  • Statik önbellek için açık tercih gerekir: export const dynamic = 'force-static' veya export const revalidate = 3600 ile etkinleştirmeniz lazım.
  • Yalnızca GET önbelleğe alınabilir: POST, PUT, DELETE gibi mutasyon metotları asla önbelleğe alınmaz — bu mantıklı.
  • Dinamik API'ler önbelleği devre dışı bırakır: cookies(), headers() veya request nesnesini okuduğunuzda rota otomatik olarak dinamik hâle gelir.
// Statik önbellekli GET Route Handler
// app/api/config/route.ts
export const revalidate = 3600 // 1 saat

export async function GET() {
  const config = await fetchAppConfig()
  return Response.json(config)
}

// use cache direktifi ile (Next.js 16)
// app/api/products/route.ts
'use cache'

export async function GET() {
  const products = await db.product.findMany({
    where: { isActive: true }
  })
  return Response.json(products)
}

Üretim Kalitesinde Tam Bir CRUD API Örneği

Şimdi gelin tüm öğrendiklerimizi bir araya getirelim. Aşağıdaki örnek, bir SaaS uygulamasının görev yönetimi API'si olarak düşünülebilir — auth, validation, rate limiting ve düzgün hata yönetimi hepsi bir arada:

// lib/validators/task.ts
import { z } from 'zod'

export const createTaskSchema = z.object({
  title: z.string().min(1, 'Başlık zorunlu').max(200),
  description: z.string().max(2000).optional(),
  priority: z.enum(['low', 'medium', 'high']),
  dueDate: z.string().datetime().optional()
})

export const updateTaskSchema = createTaskSchema.partial()

export type CreateTaskInput = z.infer<typeof createTaskSchema>
export type UpdateTaskInput = z.infer<typeof updateTaskSchema>
// app/api/tasks/route.ts
import { NextRequest, NextResponse } from 'next/server'
import { createTaskSchema } from '@/lib/validators/task'
import { rateLimit } from '@/lib/rate-limit'
import { auth } from '@/lib/auth'

export async function GET(request: NextRequest) {
  const session = await auth()
  if (!session) {
    return NextResponse.json(
      { error: 'Yetkisiz erişim' },
      { status: 401 }
    )
  }

  const searchParams = request.nextUrl.searchParams
  const page = parseInt(searchParams.get('page') ?? '1')
  const limit = parseInt(searchParams.get('limit') ?? '20')
  const priority = searchParams.get('priority')

  const where: Record<string, unknown> = {
    userId: session.user.id
  }
  if (priority) where.priority = priority

  const [tasks, total] = await Promise.all([
    db.task.findMany({
      where,
      skip: (page - 1) * limit,
      take: limit,
      orderBy: { createdAt: 'desc' }
    }),
    db.task.count({ where })
  ])

  return NextResponse.json({
    data: tasks,
    pagination: {
      page,
      limit,
      total,
      totalPages: Math.ceil(total / limit)
    }
  })
}

export async function POST(request: NextRequest) {
  const session = await auth()
  if (!session) {
    return NextResponse.json(
      { error: 'Yetkisiz erişim' },
      { status: 401 }
    )
  }

  // Rate limiting
  const ip =
    request.headers.get('x-forwarded-for')?.split(',')[0] ?? 'unknown'
  const { success } = rateLimit(ip, {
    windowMs: 60_000,
    maxRequests: 30
  })
  if (!success) {
    return NextResponse.json(
      { error: 'İstek limiti aşıldı' },
      { status: 429 }
    )
  }

  // Giriş doğrulama
  const body = await request.json()
  const result = createTaskSchema.safeParse(body)

  if (!result.success) {
    return NextResponse.json(
      {
        error: 'Doğrulama hatası',
        details: result.error.flatten().fieldErrors
      },
      { status: 400 }
    )
  }

  const task = await db.task.create({
    data: {
      ...result.data,
      userId: session.user.id
    }
  })

  return NextResponse.json(task, { status: 201 })
}
// app/api/tasks/[id]/route.ts
import { NextRequest, NextResponse } from 'next/server'
import { updateTaskSchema } from '@/lib/validators/task'
import { auth } from '@/lib/auth'

export async function GET(
  _request: NextRequest,
  { params }: { params: Promise<{ id: string }> }
) {
  const session = await auth()
  if (!session) {
    return NextResponse.json({ error: 'Yetkisiz' }, { status: 401 })
  }

  const { id } = await params
  const task = await db.task.findFirst({
    where: { id, userId: session.user.id }
  })

  if (!task) {
    return NextResponse.json(
      { error: 'Görev bulunamadı' },
      { status: 404 }
    )
  }

  return NextResponse.json(task)
}

export async function PATCH(
  request: NextRequest,
  { params }: { params: Promise<{ id: string }> }
) {
  const session = await auth()
  if (!session) {
    return NextResponse.json({ error: 'Yetkisiz' }, { status: 401 })
  }

  const { id } = await params
  const body = await request.json()
  const result = updateTaskSchema.safeParse(body)

  if (!result.success) {
    return NextResponse.json(
      { error: 'Doğrulama hatası', details: result.error.flatten().fieldErrors },
      { status: 400 }
    )
  }

  const task = await db.task.update({
    where: { id, userId: session.user.id },
    data: result.data
  })

  return NextResponse.json(task)
}

export async function DELETE(
  _request: NextRequest,
  { params }: { params: Promise<{ id: string }> }
) {
  const session = await auth()
  if (!session) {
    return NextResponse.json({ error: 'Yetkisiz' }, { status: 401 })
  }

  const { id } = await params
  await db.task.delete({
    where: { id, userId: session.user.id }
  })

  return new Response(null, { status: 204 })
}

Sıkça Sorulan Sorular

Route Handlers ile Server Actions arasındaki fark nedir?

Server Actions yalnızca POST metodu üzerinden çalışır ve dahili mutasyonlar (form gönderimleri, buton tıklamaları) için tasarlanmıştır. Route Handlers ise tüm HTTP metotlarını destekler, dış istemcilere açıktır ve yanıt üzerinde tam kontrol sağlar. Kısa versiyon: webhook, mobil API veya streaming gerekiyorsa Route Handlers; form ve buton işlemleri için Server Actions.

Next.js 16'da Route Handler'larda params neden await gerektiriyor?

Next.js 16, streaming mimarisini iyileştirmek amacıyla dinamik parametreleri Promise olarak döndürür. Parametreler böylece lazy olarak çözümlenebiliyor ve rendering pipeline ile daha iyi entegre oluyor. Eski sürümlerden geçiş yapıyorsanız, params nesnesine erişmeden önce await eklemeniz yeterli.

Route Handler'lar Edge Runtime'da çalışır mı?

Evet, gayet güzel çalışır. Route Handler dosyanıza export const runtime = 'edge' ekleyerek Edge Runtime'da çalıştırabilirsiniz. Düşük gecikme gerektiren API'ler için ideal. Ama dikkat: Edge Runtime'da tam Node.js API'leri mevcut değil — dosya sistemi erişimi ve bazı npm paketleri kullanılamaz.

Stripe webhook'unda neden request.json() yerine request.text() kullanılır?

Stripe, webhook imzasını HTTP body'nin ham (raw) string hâli üzerinden hesaplar. request.json() kullandığınızda body parse edilir ve yeniden serialize edildiğinde orijinal string ile birebir eşleşmeyebilir (boşluk ve sıralama farkları yüzünden). Sonuç? İmza doğrulaması sürekli başarısız olur. request.text() ile ham body'yi olduğu gibi alırsınız ve sorun çözülür.

SSE ile WebSocket arasında hangisini tercih etmeliyim?

SSE tek yönlü veri akışı için tasarlanmıştır — sunucudan istemciye — ve ek kütüphane gerektirmez. Bildirimler, canlı güncellemeler ve AI yanıt akışları için birebir. WebSocket ise çift yönlü iletişim gerektiren senaryolarda (sohbet, çok oyunculu oyunlar) tercih edilmeli. Açıkçası çoğu durumda SSE yeterli oluyor ve çok daha basit. Otomatik yeniden bağlanma özelliği ve HTTP/2 uyumluluğu da cabası.

Yazar Hakkında Editorial Team

Our team of expert writers and editors.