Next.js API Routes ve Server Actions: Backend İşlemleri İçin Kapsamlı Rehber
Next.js 15'te API Routes ve Server Actions ile backend işlemlerini nasıl yöneteceğinizi öğrenin. Route Handlers, form mutations, error handling ve güvenlik best practices bu detaylı rehberde.
Next.js API Routes ve Server Actions: Backend İşlemleri İçin Kapsamlı Rehber
Next.js, frontend framework olarak başladı ama artık tam teşekküllü bir full-stack framework. API Routes ile REST endpoint'leri oluşturabilir, Server Actions ile form işlemlerini doğrudan sunucuda yönetebilirsiniz. Üstelik ayrı bir backend servisi kurmadan.
Bu rehberde Next.js 15'in sunduğu backend yeteneklerini derinlemesine inceleyeceğiz. Route Handlers ile API tasarımından Server Actions ile form mutations'a, middleware'den güvenlik pattern'lerine kadar. Gerçek dünya örnekleriyle, production-ready kod snippetleriyle.
İçindekiler
- Route Handlers Nedir?
- HTTP Metodları ve Request Handling
- Dynamic API Routes
- Request ve Response API'ları
- Server Actions Temelleri
- Form Handling ile Server Actions
- useFormStatus ve useActionState
- Optimistic Updates
- Error Handling Stratejileri
- Validation ve Zod Entegrasyonu
- Authentication ve Authorization
- Rate Limiting ve Güvenlik
- Route Handlers vs Server Actions
- Sık Sorulan Sorular
Route Handlers Nedir?
Route Handlers, Next.js App Router'da API endpoint'leri oluşturmanın yolu. Pages Router'daki API Routes'un modern versiyonu. app/api/ dizininde route.js dosyalarıyla tanımlanıyor.
İlk Route Handler
// app/api/hello/route.ts
import { NextResponse } from 'next/server'
export async function GET() {
return NextResponse.json({ message: 'Merhaba Dünya!' })
}Bu endpoint /api/hello adresinde erişilebilir. Basit ama temel yapıyı gösteriyor.
Dosya Yapısı
app/
├── api/
│ ├── users/
│ │ ├── route.ts # GET /api/users, POST /api/users
│ │ └── [id]/
│ │ └── route.ts # GET /api/users/:id, PUT, DELETE
│ ├── posts/
│ │ ├── route.ts
│ │ └── [slug]/
│ │ └── route.ts
│ └── auth/
│ ├── login/
│ │ └── route.ts
│ └── logout/
│ └── route.ts
Her route.ts dosyası bir veya daha fazla HTTP metodu export edebilir.
HTTP Metodları ve Request Handling
Route Handlers, standart HTTP metodlarını destekliyor: GET, POST, PUT, PATCH, DELETE, HEAD, OPTIONS.
CRUD Operations
// app/api/products/route.ts
import { NextRequest, NextResponse } from 'next/server'
import { db } from '@/lib/database'
// GET - Tüm ürünleri listele
export async function GET(request: NextRequest) {
const searchParams = request.nextUrl.searchParams
const category = searchParams.get('category')
const limit = parseInt(searchParams.get('limit') || '10')
const offset = parseInt(searchParams.get('offset') || '0')
const products = await db.product.findMany({
where: category ? { categoryId: category } : undefined,
take: limit,
skip: offset,
include: { category: true }
})
const total = await db.product.count({
where: category ? { categoryId: category } : undefined
})
return NextResponse.json({
data: products,
pagination: {
total,
limit,
offset,
hasMore: offset + limit < total
}
})
}
// POST - Yeni ürün oluştur
export async function POST(request: NextRequest) {
try {
const body = await request.json()
const product = await db.product.create({
data: {
name: body.name,
price: body.price,
description: body.description,
categoryId: body.categoryId
}
})
return NextResponse.json(product, { status: 201 })
} catch (error) {
return NextResponse.json(
{ error: 'Ürün oluşturulamadı' },
{ status: 400 }
)
}
}Tekil Resource İşlemleri
// app/api/products/[id]/route.ts
import { NextRequest, NextResponse } from 'next/server'
import { db } from '@/lib/database'
type Params = { params: Promise<{ id: string }> }
// GET - Tek ürün getir
export async function GET(request: NextRequest, { params }: Params) {
const { id } = await params
const product = await db.product.findUnique({
where: { id },
include: { category: true, reviews: true }
})
if (!product) {
return NextResponse.json(
{ error: 'Ürün bulunamadı' },
{ status: 404 }
)
}
return NextResponse.json(product)
}
// PUT - Ürünü güncelle
export async function PUT(request: NextRequest, { params }: Params) {
const { id } = await params
const body = await request.json()
try {
const product = await db.product.update({
where: { id },
data: body
})
return NextResponse.json(product)
} catch (error) {
return NextResponse.json(
{ error: 'Ürün güncellenemedi' },
{ status: 400 }
)
}
}
// DELETE - Ürünü sil
export async function DELETE(request: NextRequest, { params }: Params) {
const { id } = await params
try {
await db.product.delete({ where: { id } })
return new NextResponse(null, { status: 204 })
} catch (error) {
return NextResponse.json(
{ error: 'Ürün silinemedi' },
{ status: 400 }
)
}
}Dynamic API Routes
Dinamik segmentler ve catch-all routes API tasarımında esneklik sağlıyor.
Parametreli Routes
// app/api/users/[userId]/posts/[postId]/route.ts
type Params = {
params: Promise<{
userId: string
postId: string
}>
}
export async function GET(request: NextRequest, { params }: Params) {
const { userId, postId } = await params
const post = await db.post.findFirst({
where: {
id: postId,
authorId: userId
}
})
if (!post) {
return NextResponse.json({ error: 'Post bulunamadı' }, { status: 404 })
}
return NextResponse.json(post)
}Catch-All Routes
// app/api/proxy/[...path]/route.ts
type Params = {
params: Promise<{ path: string[] }>
}
export async function GET(request: NextRequest, { params }: Params) {
const { path } = await params
const targetPath = path.join('/')
// External API'ye proxy
const response = await fetch(`https://external-api.com/${targetPath}`, {
headers: {
'Authorization': `Bearer ${process.env.EXTERNAL_API_KEY}`
}
})
const data = await response.json()
return NextResponse.json(data)
}Request ve Response API'ları
Next.js, Web API'larını genişleten helper'lar sunuyor.
Request İşleme
// app/api/upload/route.ts
import { NextRequest, NextResponse } from 'next/server'
export async function POST(request: NextRequest) {
// Headers
const contentType = request.headers.get('content-type')
const authorization = request.headers.get('authorization')
// Cookies
const sessionToken = request.cookies.get('session')?.value
// URL ve Search Params
const { searchParams } = request.nextUrl
const folder = searchParams.get('folder') || 'uploads'
// Form Data (multipart)
if (contentType?.includes('multipart/form-data')) {
const formData = await request.formData()
const file = formData.get('file') as File
if (!file) {
return NextResponse.json({ error: 'Dosya gerekli' }, { status: 400 })
}
const bytes = await file.arrayBuffer()
const buffer = Buffer.from(bytes)
// Dosyayı kaydet veya cloud'a yükle
const url = await uploadToStorage(buffer, file.name, folder)
return NextResponse.json({ url })
}
// JSON Body
const body = await request.json()
return NextResponse.json({ received: body })
}Response Oluşturma
// Çeşitli response türleri
export async function GET(request: NextRequest) {
const type = request.nextUrl.searchParams.get('type')
// JSON Response
if (type === 'json') {
return NextResponse.json(
{ data: 'value' },
{
status: 200,
headers: {
'Cache-Control': 'public, max-age=3600'
}
}
)
}
// Redirect
if (type === 'redirect') {
return NextResponse.redirect(new URL('/new-path', request.url))
}
// Rewrite (URL değişmez)
if (type === 'rewrite') {
return NextResponse.rewrite(new URL('/api/v2/data', request.url))
}
// Stream Response
if (type === 'stream') {
const encoder = new TextEncoder()
const stream = new ReadableStream({
async start(controller) {
for (let i = 0; i < 5; i++) {
controller.enqueue(encoder.encode(`data: ${i}\n\n`))
await new Promise(r => setTimeout(r, 1000))
}
controller.close()
}
})
return new Response(stream, {
headers: {
'Content-Type': 'text/event-stream',
'Cache-Control': 'no-cache'
}
})
}
// Plain Text
return new Response('Hello World', {
headers: { 'Content-Type': 'text/plain' }
})
}Cookie Yönetimi
// app/api/auth/login/route.ts
import { NextRequest, NextResponse } from 'next/server'
import { cookies } from 'next/headers'
export async function POST(request: NextRequest) {
const { email, password } = await request.json()
// Kullanıcı doğrulama
const user = await authenticateUser(email, password)
if (!user) {
return NextResponse.json(
{ error: 'Geçersiz credentials' },
{ status: 401 }
)
}
// Session token oluştur
const token = await createSessionToken(user.id)
// Cookie set et
const cookieStore = await cookies()
cookieStore.set('session', token, {
httpOnly: true,
secure: process.env.NODE_ENV === 'production',
sameSite: 'lax',
maxAge: 60 * 60 * 24 * 7, // 1 hafta
path: '/'
})
return NextResponse.json({ user: { id: user.id, email: user.email } })
}
// app/api/auth/logout/route.ts
export async function POST() {
const cookieStore = await cookies()
cookieStore.delete('session')
return NextResponse.json({ success: true })
}Server Actions Temelleri
Server Actions, React 19 ile gelen ve Next.js'te native desteklenen bir özellik. Form submissions ve mutations için API route yazmadan doğrudan sunucu fonksiyonlarını çağırabiliyorsunuz.
İlk Server Action
// app/actions/newsletter.ts
'use server'
import { db } from '@/lib/database'
import { revalidatePath } from 'next/cache'
export async function subscribeToNewsletter(formData: FormData) {
const email = formData.get('email') as string
if (!email || !email.includes('@')) {
return { error: 'Geçerli bir email adresi girin' }
}
try {
await db.subscriber.create({
data: { email }
})
revalidatePath('/newsletter')
return { success: true }
} catch (error) {
return { error: 'Bu email zaten kayıtlı' }
}
}Kullanım
// app/newsletter/page.tsx
import { subscribeToNewsletter } from '@/app/actions/newsletter'
export default function NewsletterPage() {
return (
<form action={subscribeToNewsletter}>
<input
type="email"
name="email"
placeholder="Email adresiniz"
required
/>
<button type="submit">Abone Ol</button>
</form>
)
}JavaScript devre dışı olsa bile form çalışır. Progressive enhancement.
Form Handling ile Server Actions
Gerçek dünya form senaryolarını inceleyelim.
Kompleks Form
// app/actions/contact.ts
'use server'
import { z } from 'zod'
import { Resend } from 'resend'
const contactSchema = z.object({
name: z.string().min(2, 'İsim en az 2 karakter olmalı'),
email: z.string().email('Geçerli email adresi girin'),
subject: z.string().min(5, 'Konu en az 5 karakter olmalı'),
message: z.string().min(20, 'Mesaj en az 20 karakter olmalı')
})
export async function submitContactForm(formData: FormData) {
const rawData = {
name: formData.get('name'),
email: formData.get('email'),
subject: formData.get('subject'),
message: formData.get('message')
}
// Validation
const result = contactSchema.safeParse(rawData)
if (!result.success) {
return {
success: false,
errors: result.error.flatten().fieldErrors
}
}
// Email gönder
const resend = new Resend(process.env.RESEND_API_KEY)
try {
await resend.emails.send({
from: 'noreply@example.com',
to: 'info@example.com',
subject: `İletişim: ${result.data.subject}`,
html: `
<p><strong>İsim:</strong> ${result.data.name}</p>
<p><strong>Email:</strong> ${result.data.email}</p>
<p><strong>Mesaj:</strong></p>
<p>${result.data.message}</p>
`
})
return { success: true }
} catch (error) {
return {
success: false,
errors: { form: ['Email gönderilemedi, lütfen tekrar deneyin'] }
}
}
}// components/contact-form.tsx
'use client'
import { useActionState } from 'react'
import { submitContactForm } from '@/app/actions/contact'
const initialState = {
success: false,
errors: {}
}
export function ContactForm() {
const [state, formAction, isPending] = useActionState(
submitContactForm,
initialState
)
if (state.success) {
return (
<div className="p-4 bg-green-100 text-green-800 rounded">
Mesajınız başarıyla gönderildi!
</div>
)
}
return (
<form action={formAction} className="space-y-4">
<div>
<label htmlFor="name">İsim</label>
<input
type="text"
id="name"
name="name"
className="w-full border rounded p-2"
/>
{state.errors?.name && (
<p className="text-red-500 text-sm">{state.errors.name[0]}</p>
)}
</div>
<div>
<label htmlFor="email">Email</label>
<input
type="email"
id="email"
name="email"
className="w-full border rounded p-2"
/>
{state.errors?.email && (
<p className="text-red-500 text-sm">{state.errors.email[0]}</p>
)}
</div>
<div>
<label htmlFor="subject">Konu</label>
<input
type="text"
id="subject"
name="subject"
className="w-full border rounded p-2"
/>
{state.errors?.subject && (
<p className="text-red-500 text-sm">{state.errors.subject[0]}</p>
)}
</div>
<div>
<label htmlFor="message">Mesaj</label>
<textarea
id="message"
name="message"
rows={5}
className="w-full border rounded p-2"
/>
{state.errors?.message && (
<p className="text-red-500 text-sm">{state.errors.message[0]}</p>
)}
</div>
{state.errors?.form && (
<div className="p-3 bg-red-100 text-red-800 rounded">
{state.errors.form[0]}
</div>
)}
<button
type="submit"
disabled={isPending}
className="w-full bg-primary text-white py-2 rounded disabled:opacity-50"
>
{isPending ? 'Gönderiliyor...' : 'Gönder'}
</button>
</form>
)
}useFormStatus ve useActionState
React 19'un form hook'ları Server Actions ile mükemmel çalışıyor.
useFormStatus
'use client'
import { useFormStatus } from 'react-dom'
function SubmitButton({ children }: { children: React.ReactNode }) {
const { pending, data, method, action } = useFormStatus()
return (
<button
type="submit"
disabled={pending}
className="btn-primary disabled:opacity-50"
>
{pending ? (
<span className="flex items-center gap-2">
<Spinner className="w-4 h-4" />
İşleniyor...
</span>
) : (
children
)}
</button>
)
}
// Kullanım
export function MyForm({ action }) {
return (
<form action={action}>
<input name="title" required />
<SubmitButton>Kaydet</SubmitButton>
</form>
)
}useFormStatus parent form'un durumunu otomatik takip ediyor. Component form içinde olmalı.
useActionState
'use client'
import { useActionState } from 'react'
import { createPost } from '@/app/actions/posts'
type State = {
message: string | null
errors: Record<string, string[]>
}
const initialState: State = {
message: null,
errors: {}
}
export function CreatePostForm() {
const [state, formAction, isPending] = useActionState(
createPost,
initialState
)
return (
<form action={formAction}>
<div>
<input name="title" placeholder="Başlık" />
{state.errors.title?.map((error, i) => (
<p key={i} className="text-red-500">{error}</p>
))}
</div>
<div>
<textarea name="content" placeholder="İçerik" />
{state.errors.content?.map((error, i) => (
<p key={i} className="text-red-500">{error}</p>
))}
</div>
{state.message && (
<div className="p-3 bg-green-100 text-green-800 rounded">
{state.message}
</div>
)}
<button type="submit" disabled={isPending}>
{isPending ? 'Kaydediliyor...' : 'Yayınla'}
</button>
</form>
)
}
// app/actions/posts.ts
'use server'
export async function createPost(prevState: State, formData: FormData) {
const title = formData.get('title') as string
const content = formData.get('content') as string
const errors: Record<string, string[]> = {}
if (!title || title.length < 5) {
errors.title = ['Başlık en az 5 karakter olmalı']
}
if (!content || content.length < 50) {
errors.content = ['İçerik en az 50 karakter olmalı']
}
if (Object.keys(errors).length > 0) {
return { message: null, errors }
}
await db.post.create({ data: { title, content } })
revalidatePath('/posts')
return { message: 'Post başarıyla oluşturuldu!', errors: {} }
}Optimistic Updates
UI'ı server response beklemeden güncellemek için useOptimistic kullanıyoruz.
'use client'
import { useOptimistic, useTransition } from 'react'
import { toggleLike } from '@/app/actions/likes'
type Post = {
id: string
title: string
likes: number
isLiked: boolean
}
export function PostCard({ post }: { post: Post }) {
const [isPending, startTransition] = useTransition()
const [optimisticPost, addOptimistic] = useOptimistic(
post,
(state, newLikeState: boolean) => ({
...state,
isLiked: newLikeState,
likes: newLikeState ? state.likes + 1 : state.likes - 1
})
)
async function handleLikeClick() {
const newState = !optimisticPost.isLiked
startTransition(async () => {
addOptimistic(newState)
await toggleLike(post.id)
})
}
return (
<div className="p-4 border rounded">
<h2>{optimisticPost.title}</h2>
<button
onClick={handleLikeClick}
disabled={isPending}
className={`flex items-center gap-2 ${
optimisticPost.isLiked ? 'text-red-500' : 'text-gray-500'
}`}
>
<HeartIcon filled={optimisticPost.isLiked} />
<span>{optimisticPost.likes}</span>
</button>
</div>
)
}Kullanıcı beğen butonuna tıkladığında UI anında güncellenir. Server error olursa React otomatik rollback yapar.
Error Handling Stratejileri
Server Action Error Handling
// app/actions/users.ts
'use server'
import { z } from 'zod'
const updateProfileSchema = z.object({
name: z.string().min(2),
bio: z.string().max(500).optional()
})
export async function updateProfile(formData: FormData) {
// Auth check
const session = await getServerSession()
if (!session) {
return { error: 'Oturum açmanız gerekiyor', code: 'UNAUTHORIZED' }
}
// Validation
const rawData = Object.fromEntries(formData)
const parsed = updateProfileSchema.safeParse(rawData)
if (!parsed.success) {
return {
error: 'Validasyon hatası',
code: 'VALIDATION_ERROR',
details: parsed.error.flatten().fieldErrors
}
}
// Database operation
try {
await db.user.update({
where: { id: session.user.id },
data: parsed.data
})
revalidatePath('/profile')
return { success: true }
} catch (error) {
// Prisma unique constraint error
if (error.code === 'P2002') {
return { error: 'Bu isim zaten kullanılıyor', code: 'DUPLICATE' }
}
// Log unexpected errors
console.error('Profile update failed:', error)
return { error: 'Beklenmeyen bir hata oluştu', code: 'INTERNAL_ERROR' }
}
}Route Handler Error Handling
// app/api/products/route.ts
import { NextRequest, NextResponse } from 'next/server'
import { z } from 'zod'
const productSchema = z.object({
name: z.string().min(2),
price: z.number().positive(),
categoryId: z.string().uuid()
})
export async function POST(request: NextRequest) {
try {
// Auth check
const session = await getServerSession()
if (!session) {
return NextResponse.json(
{ error: 'Unauthorized', code: 'UNAUTHORIZED' },
{ status: 401 }
)
}
// Parse body
let body
try {
body = await request.json()
} catch {
return NextResponse.json(
{ error: 'Invalid JSON', code: 'INVALID_JSON' },
{ status: 400 }
)
}
// Validate
const parsed = productSchema.safeParse(body)
if (!parsed.success) {
return NextResponse.json(
{
error: 'Validation failed',
code: 'VALIDATION_ERROR',
details: parsed.error.flatten().fieldErrors
},
{ status: 400 }
)
}
// Create product
const product = await db.product.create({
data: parsed.data
})
return NextResponse.json(product, { status: 201 })
} catch (error) {
console.error('Product creation failed:', error)
return NextResponse.json(
{ error: 'Internal server error', code: 'INTERNAL_ERROR' },
{ status: 500 }
)
}
}Validation ve Zod Entegrasyonu
Zod, TypeScript-first schema validation kütüphanesi. Server Actions ve Route Handlers ile mükemmel çalışıyor.
Schema Tanımlama
// lib/validations/product.ts
import { z } from 'zod'
export const productSchema = z.object({
name: z
.string()
.min(2, 'Ürün adı en az 2 karakter olmalı')
.max(100, 'Ürün adı en fazla 100 karakter olabilir'),
description: z
.string()
.min(10, 'Açıklama en az 10 karakter olmalı')
.max(1000, 'Açıklama en fazla 1000 karakter olabilir'),
price: z
.number()
.positive('Fiyat pozitif olmalı')
.max(1000000, 'Fiyat çok yüksek'),
stock: z
.number()
.int('Stok tam sayı olmalı')
.min(0, 'Stok negatif olamaz'),
categoryId: z
.string()
.uuid('Geçerli kategori seçin'),
images: z
.array(z.string().url())
.min(1, 'En az 1 görsel gerekli')
.max(10, 'En fazla 10 görsel yükleyebilirsiniz'),
tags: z
.array(z.string())
.optional()
.default([])
})
export type ProductInput = z.infer<typeof productSchema>
// Partial schema for updates
export const productUpdateSchema = productSchema.partial()FormData ile Kullanım
// app/actions/products.ts
'use server'
import { productSchema } from '@/lib/validations/product'
export async function createProduct(formData: FormData) {
// FormData'yı object'e çevir
const rawData = {
name: formData.get('name'),
description: formData.get('description'),
price: parseFloat(formData.get('price') as string),
stock: parseInt(formData.get('stock') as string),
categoryId: formData.get('categoryId'),
images: formData.getAll('images'),
tags: formData.getAll('tags')
}
const result = productSchema.safeParse(rawData)
if (!result.success) {
return {
success: false,
errors: result.error.flatten().fieldErrors
}
}
// result.data tam typed
const product = await db.product.create({
data: result.data
})
revalidatePath('/products')
return { success: true, product }
}Authentication ve Authorization
API güvenliği için authentication ve authorization pattern'leri.
Session Kontrolü
// lib/auth.ts
import { cookies } from 'next/headers'
import { db } from './database'
export async function getSession() {
const cookieStore = await cookies()
const token = cookieStore.get('session')?.value
if (!token) return null
const session = await db.session.findUnique({
where: { token },
include: { user: true }
})
if (!session || session.expiresAt < new Date()) {
return null
}
return session
}
export async function requireAuth() {
const session = await getSession()
if (!session) {
throw new Error('Unauthorized')
}
return session
}
export async function requireRole(role: string) {
const session = await requireAuth()
if (session.user.role !== role) {
throw new Error('Forbidden')
}
return session
}Protected Route Handler
// app/api/admin/users/route.ts
import { NextRequest, NextResponse } from 'next/server'
import { requireRole } from '@/lib/auth'
export async function GET(request: NextRequest) {
try {
const session = await requireRole('admin')
const users = await db.user.findMany({
select: {
id: true,
email: true,
name: true,
role: true,
createdAt: true
}
})
return NextResponse.json(users)
} catch (error) {
if (error.message === 'Unauthorized') {
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
}
if (error.message === 'Forbidden') {
return NextResponse.json({ error: 'Forbidden' }, { status: 403 })
}
return NextResponse.json({ error: 'Server error' }, { status: 500 })
}
}Protected Server Action
// app/actions/admin.ts
'use server'
import { requireRole } from '@/lib/auth'
import { redirect } from 'next/navigation'
export async function deleteUser(userId: string) {
try {
await requireRole('admin')
} catch {
redirect('/login')
}
await db.user.delete({ where: { id: userId } })
revalidatePath('/admin/users')
return { success: true }
}Rate Limiting ve Güvenlik
Basit Rate Limiter
// lib/rate-limit.ts
const requests = new Map<string, { count: number; resetAt: number }>()
export function rateLimit(
identifier: string,
limit: number = 10,
windowMs: number = 60000
) {
const now = Date.now()
const record = requests.get(identifier)
if (!record || record.resetAt < now) {
requests.set(identifier, { count: 1, resetAt: now + windowMs })
return { success: true, remaining: limit - 1 }
}
if (record.count >= limit) {
return {
success: false,
remaining: 0,
retryAfter: Math.ceil((record.resetAt - now) / 1000)
}
}
record.count++
return { success: true, remaining: limit - record.count }
}Rate Limited Route Handler
// app/api/contact/route.ts
import { NextRequest, NextResponse } from 'next/server'
import { rateLimit } from '@/lib/rate-limit'
export async function POST(request: NextRequest) {
// IP bazlı rate limiting
const ip = request.headers.get('x-forwarded-for') || 'unknown'
const { success, remaining, retryAfter } = rateLimit(ip, 5, 60000)
if (!success) {
return NextResponse.json(
{ error: 'Too many requests' },
{
status: 429,
headers: {
'Retry-After': String(retryAfter),
'X-RateLimit-Remaining': '0'
}
}
)
}
// Normal işlem devam...
const body = await request.json()
return NextResponse.json(
{ success: true },
{
headers: {
'X-RateLimit-Remaining': String(remaining)
}
}
)
}Güvenlik Headers
// middleware.ts
import { NextResponse } from 'next/server'
import type { NextRequest } from 'next/server'
export function middleware(request: NextRequest) {
const response = NextResponse.next()
// Security headers
response.headers.set('X-Content-Type-Options', 'nosniff')
response.headers.set('X-Frame-Options', 'DENY')
response.headers.set('X-XSS-Protection', '1; mode=block')
response.headers.set('Referrer-Policy', 'strict-origin-when-cross-origin')
// API routes için CORS
if (request.nextUrl.pathname.startsWith('/api/')) {
response.headers.set('Access-Control-Allow-Origin', process.env.ALLOWED_ORIGIN || '*')
response.headers.set('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE, OPTIONS')
response.headers.set('Access-Control-Allow-Headers', 'Content-Type, Authorization')
}
return response
}
export const config = {
matcher: ['/api/:path*', '/((?!_next/static|favicon.ico).*)']
}Route Handlers vs Server Actions
Ne zaman hangisini kullanmalı?
Route Handlers Kullanın
- External API'ler için (webhook'lar, 3rd party entegrasyonlar)
- Non-form mutations (click handlers, programmatic calls)
- Streaming responses (SSE, file downloads)
- Public API oluşturuyorsanız
// Webhook endpoint
// app/api/webhooks/stripe/route.ts
export async function POST(request: NextRequest) {
const body = await request.text()
const signature = request.headers.get('stripe-signature')
const event = stripe.webhooks.constructEvent(body, signature, webhookSecret)
switch (event.type) {
case 'payment_intent.succeeded':
await handlePaymentSuccess(event.data.object)
break
}
return NextResponse.json({ received: true })
}Server Actions Kullanın
- Form submissions
- UI mutations (like, delete, update)
- Optimistic updates gerektiren işlemler
- Progressive enhancement istediğinizde
// Delete action with optimistic update
'use server'
export async function deletePost(id: string) {
await db.post.delete({ where: { id } })
revalidatePath('/posts')
}Sık Sorulan Sorular
Server Actions güvenli mi?
Evet. Next.js, Server Actions için otomatik CSRF koruması sağlıyor. Actions sadece POST istekleriyle çağrılabilir ve encrypted action ID'ler kullanılır.
Route Handlers cache'leniyor mu?
GET metodları varsayılan olarak cache'lenmez (Next.js 15'te). Cache istiyorsanız export const dynamic = 'force-static' veya fetch options kullanın.
Server Actions'da file upload yapabilir miyim?
Evet, FormData üzerinden file alabilirsiniz. Ancak büyük dosyalar için Route Handlers veya external upload servisleri önerilir.
API routes'u test etmenin en iyi yolu nedir?
Route Handlers için Vitest veya Jest ile unit test yazabilirsiniz. Server Actions için React Testing Library ile integration test önerilir.
WebSocket kullanabilir miyim?
Next.js native WebSocket desteği sunmuyor. Socket.io veya Pusher gibi external servisler veya custom server setup gerekir.
Rate limiting için Redis kullanmalı mıyım?
Production'da evet. In-memory rate limiting tek instance için çalışır ama multiple instance'larda Redis veya benzeri distributed cache şart.
Sonuç
Next.js 15, Route Handlers ve Server Actions ile full-stack development'ı ciddi anlamda kolaylaştırıyor. API tasarımından form handling'e, authentication'dan güvenliğe kadar tüm backend ihtiyaçlarınızı karşılayabilirsiniz.
Server Actions özellikle form-heavy uygulamalarda game changer. Optimistic updates, progressive enhancement ve type safety bir arada. Route Handlers ise webhook'lar, external entegrasyonlar ve public API'ler için ideal.
Sonraki Adımlar:
- Basit bir CRUD API oluşturun
- Server Actions ile form handling deneyin
- Zod ile validation ekleyin
- Authentication middleware yazın
- Rate limiting implement edin
Serinin devamında Prisma ve PostgreSQL entegrasyonunu işleyeceğiz. Database işlemlerini Next.js ile nasıl yöneteceğinizi öğreneceksiniz.
Projenizi Hayata Geçirelim
Web sitesi, mobil uygulama veya yapay zeka çözümü mü arıyorsunuz? Fikirlerinizi birlikte değerlendirelim.
Ücretsiz Danışmanlık Alın