Giriş
Next.js, React ekosisteminin en güçlü framework'lerinden biri ve sürekli evrim geçiriyor. Next.js 13 ile gelen App Router mimarisi, sunucu tarafı rendering ile istemci tarafı etkileşimi aynı çatı altında yönetmeyi mümkün kıldı. Ama bence asıl devrim, Server Actions ile geldi.
Next.js 14'te kararlı hale gelen ve 15/16 sürümleriyle iyice olgunlaşan Server Actions, full-stack React uygulamaları geliştirme biçimimizi kökten değiştirdi.
Bir düşünün: geleneksel yaklaşımda basit bir form gönderimi için bile API route oluşturmak, istemcide fetch yazmak, loading state'ini yönetmek, hata işleme mantığı kurmak ve önbellek stratejisi belirlemek gerekiyordu. Server Actions tüm bu katmanları ortadan kaldırarak sunucu tarafı mutasyonlarını doğrudan React bileşenlerinin içinden çağırmanıza olanak tanıyor. Bu sadece daha az kod yazmak değil; daha güvenli, daha performanslı ve bakımı daha kolay uygulamalar inşa etmek demek.
Bu rehberde, Server Actions'ın temellerinden ileri düzey kalıplara kadar her şeyi ele alacağız. Güvenlik best practice'leri, Zod ile doğrulama, önbellek yönetimi, veritabanı işlemleri ve optimistik güncellemeler gibi konulara derinlemesine dalacağız. İster Next.js'e yeni başlıyor olun ister mevcut projelerinizi modernize etmek istiyor olun, bu rehber size sağlam bir temel sunacak.
Server Actions Nedir?
Server Actions, React'in sunucu tarafında çalışan asenkron fonksiyonlarıdır. "use server" direktifi ile işaretlenen bu fonksiyonlar istemci bileşenlerinden doğrudan çağrılabilir, ama her zaman sunucuda yürütülür. Geleneksel API endpoint'lerine olan ihtiyacı büyük ölçüde ortadan kaldıran bir mekanizma.
Perde Arkasında Ne Oluyor?
Bir Server Action tanımladığınızda, Next.js derleme aşamasında birkaç kritik işlem gerçekleştirir:
- Benzersiz kimlik atama: Her server action'a non-deterministic benzersiz bir kimlik atanır. İstemcinin hangi sunucu fonksiyonunu çağıracağını belirlemek için bu kimlik kullanılır.
- HTTP POST endpoint oluşturma: Her server action otomatik olarak bir HTTP POST endpoint'ine dönüşür. İstemci tarafından yapılan çağrılar bu endpoint'e POST istekleri olarak gider.
- Serileştirme: Argümanlar ve dönüş değerleri React'in serileştirme protokolü aracılığıyla aktarılır. Yani yalnızca serileştirilebilir değerler (string, number, boolean, Date, FormData, plain object, array vb.) taşınabilir.
- Güvenlik katmanı: Next.js CSRF korumasını otomatik olarak sağlar. Her server action çağrısı geçerli bir origin başlığı kontrolünden geçer.
"use server" Direktifi
"use server" direktifi iki farklı şekilde kullanılabiliyor. Birincisi, dosyanın en üstüne yerleştirerek o dosyadaki tüm export edilen fonksiyonları server action olarak işaretleyebilirsiniz. İkincisi ise bir asenkron fonksiyonun gövdesinin başına koyarak sadece o fonksiyonu server action yapabilirsiniz.
// Dosya düzeyinde kullanım: app/actions.ts
"use server";
// Bu dosyadaki tüm export edilen fonksiyonlar server action'dır
export async function kullaniciyiKaydet(formData: FormData) {
// Sunucuda çalışır
const isim = formData.get("isim") as string;
// Veritabanı işlemi...
}
export async function kullaniciyiSil(id: string) {
// Bu da sunucuda çalışır
// Veritabanı işlemi...
}
Burada çok önemli bir nokta var: "use server" direktifi fonksiyonun sunucuda çalışacağını garanti eder ama güvenli olduğunu garanti etmez. Her server action, potansiyel olarak herhangi bir istemci tarafından çağrılabilecek açık bir API endpoint'idir. Bu yüzden her zaman giriş doğrulaması ve yetkilendirme kontrolü yapmanız şart.
Server Actions vs API Routes
Server Actions ve Route Handlers (API Routes) aslında farklı kullanım senaryoları için tasarlanmış. Server Actions form gönderimleri ve veri mutasyonları için ideal: doğrudan bileşenlerden çağrılabiliyor, progressive enhancement'ı destekliyor ve React'in önbellek mekanizmasıyla sorunsuz çalışıyor. Route Handlers ise harici API'ler oluşturmak, webhook'ları işlemek veya üçüncü taraf entegrasyonları için daha uygun.
İlk Server Action'ınızı Oluşturma
Haydi, adım adım ilk server action'ımızı oluşturalım. İki farklı yaklaşımı inceleyeceğiz: ayrı dosyada tanımlama ve satır içi (inline) tanımlama.
Yaklaşım 1: Ayrı Dosyada Tanımlama (Önerilen)
En yaygın ve önerilen yaklaşım, server action'larınızı ayrı bir dosyada tanımlamak. Kodunuzu temiz tutar, yeniden kullanılabilirliği artırır.
// app/actions/mesaj-actions.ts
"use server";
import { revalidatePath } from "next/cache";
interface MesajSonucu {
basarili: boolean;
mesaj: string;
}
export async function mesajGonder(formData: FormData): Promise<MesajSonucu> {
// Sunucu tarafında çalışır
const icerik = formData.get("icerik") as string;
const gonderenId = formData.get("gonderenId") as string;
if (!icerik || icerik.trim().length === 0) {
return {
basarili: false,
mesaj: "Mesaj içeriği boş olamaz.",
};
}
try {
// Veritabanına kaydetme simülasyonu
await new Promise((resolve) => setTimeout(resolve, 1000));
console.log(`Yeni mesaj kaydedildi: ${icerik} (Gönderen: ${gonderenId})`);
// İlgili sayfanın önbelleğini geçersiz kıl
revalidatePath("/mesajlar");
return {
basarili: true,
mesaj: "Mesajınız başarıyla gönderildi!",
};
} catch (hata) {
return {
basarili: false,
mesaj: "Mesaj gönderilirken bir hata oluştu.",
};
}
}
Şimdi bu action'ı bir bileşende kullanalım:
// app/mesajlar/page.tsx
import { mesajGonder } from "@/app/actions/mesaj-actions";
export default function MesajlarSayfasi() {
return (
<div className="max-w-2xl mx-auto p-6">
<h1 className="text-2xl font-bold mb-4">Mesaj Gönder</h1>
<form action={mesajGonder} className="space-y-4">
<input type="hidden" name="gonderenId" value="kullanici-123" />
<div>
<label htmlFor="icerik" className="block text-sm font-medium">
Mesajınız
</label>
<textarea
id="icerik"
name="icerik"
rows={4}
className="mt-1 block w-full rounded-md border p-2"
placeholder="Mesajınızı yazın..."
required
/>
</div>
<button
type="submit"
className="bg-blue-600 text-white px-4 py-2 rounded-md hover:bg-blue-700"
>
Gönder
</button>
</form>
</div>
);
}
Yaklaşım 2: Sunucu Bileşeninde Satır İçi Tanımlama
Server Component'larda fonksiyonun gövdesine "use server" direktifini ekleyerek satır içi server action tanımlayabilirsiniz:
// app/abone/page.tsx
import { redirect } from "next/navigation";
export default function AboneSayfasi() {
async function aboneOl(formData: FormData) {
"use server";
const email = formData.get("email") as string;
// E-posta doğrulama
if (!email || !email.includes("@")) {
throw new Error("Geçerli bir e-posta adresi giriniz.");
}
// Veritabanına kaydet
// await db.abone.create({ data: { email } });
redirect("/abone/tesekkurler");
}
return (
<form action={aboneOl} className="flex gap-2">
<input
type="email"
name="email"
placeholder="E-posta adresiniz"
className="border rounded px-3 py-2"
required
/>
<button type="submit" className="bg-green-600 text-white px-4 py-2 rounded">
Abone Ol
</button>
</form>
);
}
Satır içi tanımlama küçük ve tek kullanımlık action'lar için gayet pratik. Ama karmaşık iş mantığı veya birden fazla bileşende yeniden kullanım gerektiren durumlarda ayrı dosya yaklaşımını tercih etmenizi öneririm.
Form İşleme ve useActionState Hook'u
Gerçek dünya uygulamalarında form gönderiminin sonucunu kullanıcıya göstermek, loading state'ini yönetmek ve hata mesajlarını görüntülemek gerekiyor. İşte tam bu noktada React 19 ile gelen useActionState hook'u (eski adıyla useFormState) devreye giriyor.
useActionState Hook'u
useActionState, bir server action'ı sarmalayarak üç önemli değer döndürür: mevcut state, sarmalanmış action fonksiyonu ve pending durumu.
// app/actions/iletisim-actions.ts
"use server";
export interface IletisimFormDurumu {
basarili: boolean;
mesaj: string;
hatalar?: {
isim?: string[];
email?: string[];
konu?: string[];
icerik?: string[];
};
}
export async function iletisimFormuGonder(
oncekiDurum: IletisimFormDurumu,
formData: FormData
): Promise<IletisimFormDurumu> {
const isim = formData.get("isim") as string;
const email = formData.get("email") as string;
const konu = formData.get("konu") as string;
const icerik = formData.get("icerik") as string;
// Basit doğrulama
const hatalar: IletisimFormDurumu["hatalar"] = {};
if (!isim || isim.trim().length < 2) {
hatalar.isim = ["İsim en az 2 karakter olmalıdır."];
}
if (!email || !email.includes("@")) {
hatalar.email = ["Geçerli bir e-posta adresi giriniz."];
}
if (!konu || konu.trim().length < 5) {
hatalar.konu = ["Konu en az 5 karakter olmalıdır."];
}
if (!icerik || icerik.trim().length < 20) {
hatalar.icerik = ["Mesaj en az 20 karakter olmalıdır."];
}
if (Object.keys(hatalar).length > 0) {
return {
basarili: false,
mesaj: "Lütfen formdaki hataları düzeltin.",
hatalar,
};
}
try {
// E-posta gönderme veya veritabanı kaydı simülasyonu
await new Promise((resolve) => setTimeout(resolve, 2000));
return {
basarili: true,
mesaj: "Mesajınız başarıyla gönderildi! En kısa sürede dönüş yapacağız.",
};
} catch (hata) {
return {
basarili: false,
mesaj: "Bir hata oluştu. Lütfen daha sonra tekrar deneyin.",
};
}
}
Şimdi bu action'ı useActionState ile kullanan istemci bileşenini oluşturalım:
// app/iletisim/iletisim-formu.tsx
"use client";
import { useActionState } from "react";
import { iletisimFormuGonder, type IletisimFormDurumu } from "@/app/actions/iletisim-actions";
const baslangicDurumu: IletisimFormDurumu = {
basarili: false,
mesaj: "",
};
export function IletisimFormu() {
const [durum, formAction, bekleniyor] = useActionState(
iletisimFormuGonder,
baslangicDurumu
);
return (
<form action={formAction} className="space-y-6 max-w-lg">
{/* Durum mesajı */}
{durum.mesaj && (
<div
className={`p-4 rounded-md ${
durum.basarili
? "bg-green-50 text-green-800 border border-green-200"
: "bg-red-50 text-red-800 border border-red-200"
}`}
>
{durum.mesaj}
</div>
)}
{/* İsim alanı */}
<div>
<label htmlFor="isim" className="block text-sm font-medium">
Adınız Soyadınız
</label>
<input
type="text"
id="isim"
name="isim"
className="mt-1 block w-full rounded-md border p-2"
disabled={bekleniyor}
/>
{durum.hatalar?.isim && (
<p className="mt-1 text-sm text-red-600">{durum.hatalar.isim[0]}</p>
)}
</div>
{/* E-posta alanı */}
<div>
<label htmlFor="email" className="block text-sm font-medium">
E-posta Adresi
</label>
<input
type="email"
id="email"
name="email"
className="mt-1 block w-full rounded-md border p-2"
disabled={bekleniyor}
/>
{durum.hatalar?.email && (
<p className="mt-1 text-sm text-red-600">{durum.hatalar.email[0]}</p>
)}
</div>
{/* Konu alanı */}
<div>
<label htmlFor="konu" className="block text-sm font-medium">
Konu
</label>
<input
type="text"
id="konu"
name="konu"
className="mt-1 block w-full rounded-md border p-2"
disabled={bekleniyor}
/>
{durum.hatalar?.konu && (
<p className="mt-1 text-sm text-red-600">{durum.hatalar.konu[0]}</p>
)}
</div>
{/* Mesaj alanı */}
<div>
<label htmlFor="icerik" className="block text-sm font-medium">
Mesajınız
</label>
<textarea
id="icerik"
name="icerik"
rows={5}
className="mt-1 block w-full rounded-md border p-2"
disabled={bekleniyor}
/>
{durum.hatalar?.icerik && (
<p className="mt-1 text-sm text-red-600">{durum.hatalar.icerik[0]}</p>
)}
</div>
{/* Gönder düğmesi */}
<button
type="submit"
disabled={bekleniyor}
className="w-full bg-blue-600 text-white py-2 px-4 rounded-md
hover:bg-blue-700 disabled:opacity-50 disabled:cursor-not-allowed
transition-colors"
>
{bekleniyor ? "Gönderiliyor..." : "Mesajı Gönder"}
</button>
</form>
);
}
useActionState hook'unun çalışma mantığı şöyle: ilk parametre olarak server action fonksiyonunu alır (bu fonksiyonun ilk parametresi önceki durum, ikincisi FormData olmalı). İkinci parametre başlangıç durumu. Döndürdüğü üçlüdeki bekleniyor değeri formun gönderim sürecinde olup olmadığını bildirir — böylece loading göstergesi veya form alanlarını devre dışı bırakma gibi UX iyileştirmelerini kolayca uygulayabilirsiniz.
Zod ile Veri Doğrulama
Üretim ortamında veri doğrulama en kritik güvenlik katmanlarından biri. Açıkçası, bu konuda Zod gerçekten harika bir iş çıkarıyor. TypeScript ile mükemmel uyum sağlayan bu şema doğrulama kütüphanesi, Server Actions ile birlikte kullanıldığında hem tip güvenliği hem de runtime doğrulaması sunuyor.
Zod Şeması Oluşturma
// lib/validasyon/kullanici-semasi.ts
import { z } from "zod";
export const kullaniciOlusturmaSemasi = z.object({
isim: z
.string()
.min(2, "İsim en az 2 karakter olmalıdır.")
.max(50, "İsim en fazla 50 karakter olabilir.")
.trim(),
email: z
.string()
.email("Geçerli bir e-posta adresi giriniz.")
.toLowerCase(),
sifre: z
.string()
.min(8, "Şifre en az 8 karakter olmalıdır.")
.max(128, "Şifre en fazla 128 karakter olabilir.")
.regex(
/^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)/,
"Şifre en az bir küçük harf, bir büyük harf ve bir rakam içermelidir."
),
sifreOnay: z.string(),
yas: z
.number()
.int("Yaş tam sayı olmalıdır.")
.min(18, "En az 18 yaşında olmalısınız.")
.max(120, "Geçerli bir yaş giriniz."),
telefon: z
.string()
.regex(
/^(\+90|0)?[0-9]{10}$/,
"Geçerli bir Türkiye telefon numarası giriniz."
)
.optional(),
}).refine((veri) => veri.sifre === veri.sifreOnay, {
message: "Şifreler eşleşmiyor.",
path: ["sifreOnay"],
});
// Tip çıkarımı
export type KullaniciOlusturmaGirdisi = z.infer<typeof kullaniciOlusturmaSemasi>;
Server Action'da Zod Kullanımı
// app/actions/kullanici-actions.ts
"use server";
import { kullaniciOlusturmaSemasi } from "@/lib/validasyon/kullanici-semasi";
import { revalidatePath } from "next/cache";
export interface KullaniciFormDurumu {
basarili: boolean;
mesaj: string;
hatalar?: Record<string, string[]>;
}
export async function kullaniciOlustur(
oncekiDurum: KullaniciFormDurumu,
formData: FormData
): Promise<KullaniciFormDurumu> {
// FormData'dan ham verileri çıkar
const hamVeri = {
isim: formData.get("isim") as string,
email: formData.get("email") as string,
sifre: formData.get("sifre") as string,
sifreOnay: formData.get("sifreOnay") as string,
yas: Number(formData.get("yas")),
telefon: (formData.get("telefon") as string) || undefined,
};
// Zod ile doğrulama - safeParse hata fırlatmaz
const dogrulamaSonucu = kullaniciOlusturmaSemasi.safeParse(hamVeri);
if (!dogrulamaSonucu.success) {
// Alan düzeyinde hataları düzenle
const alanHatalari = dogrulamaSonucu.error.flatten().fieldErrors;
return {
basarili: false,
mesaj: "Lütfen formdaki hataları düzeltin.",
hatalar: alanHatalari as Record<string, string[]>,
};
}
// Doğrulanmış veriyi kullan - artık tip güvenli
const dogrulanmisVeri = dogrulamaSonucu.data;
try {
// Veritabanına kaydet
// await prisma.kullanici.create({
// data: {
// isim: dogrulanmisVeri.isim,
// email: dogrulanmisVeri.email,
// sifre: await hashle(dogrulanmisVeri.sifre),
// yas: dogrulanmisVeri.yas,
// telefon: dogrulanmisVeri.telefon,
// },
// });
// Simülasyon
await new Promise((resolve) => setTimeout(resolve, 1500));
revalidatePath("/kullanicilar");
return {
basarili: true,
mesaj: "Hesabınız başarıyla oluşturuldu!",
};
} catch (hata: unknown) {
// Benzersizlik hatası kontrolü
if (
hata instanceof Error &&
hata.message.includes("Unique constraint")
) {
return {
basarili: false,
mesaj: "Bu e-posta adresi zaten kayıtlı.",
hatalar: {
email: ["Bu e-posta adresi zaten kullanımda."],
},
};
}
return {
basarili: false,
mesaj: "Bir hata oluştu. Lütfen daha sonra tekrar deneyin.",
};
}
}
Zod'un safeParse yöntemi, doğrulama başarısız olduğunda hata fırlatmak yerine bir sonuç nesnesi döndürüyor. Bu da hata işleme mantığını temiz tutuyor ve alan bazında hataları kolayca elde etmenizi sağlıyor. flatten() yöntemi ise hataları { alanAdi: string[] } formatına dönüştürüyor — form bileşenlerinde kolayca kullanılabilir bir yapı.
Yeniden Kullanılabilir Doğrulama Yardımcısı
Birden fazla server action'da aynı doğrulama mantığını tekrar tekrar yazmak istemezsiniz (ben de istemem, açıkçası). Bunun için genel amaçlı bir yardımcı fonksiyon oluşturabilirsiniz:
// lib/validasyon/dogrulama-yardimcisi.ts
import { z } from "zod";
export interface DogrulamaSonucu<T> {
basarili: true;
veri: T;
} | {
basarili: false;
hatalar: Record<string, string[]>;
}
export function formVerisiniDogrula<T>(
sema: z.ZodSchema<T>,
formData: FormData,
alanlar: Record<string, "string" | "number" | "boolean">
): DogrulamaSonucu<T> {
// FormData'dan ham nesne oluştur
const hamVeri: Record<string, unknown> = {};
for (const [alan, tip] of Object.entries(alanlar)) {
const deger = formData.get(alan);
if (tip === "number") {
hamVeri[alan] = deger ? Number(deger) : undefined;
} else if (tip === "boolean") {
hamVeri[alan] = deger === "on" || deger === "true";
} else {
hamVeri[alan] = deger;
}
}
const sonuc = sema.safeParse(hamVeri);
if (!sonuc.success) {
return {
basarili: false,
hatalar: sonuc.error.flatten().fieldErrors as Record<string, string[]>,
};
}
return {
basarili: true,
veri: sonuc.data,
};
}
Güvenlik En İyi Uygulamaları
Server Actions sunucu tarafında çalışsa da otomatik olarak güvenli değildir. Her server action'ı dışarıya açık bir API endpoint'i gibi düşünmeniz gerekiyor. İşte dikkat etmeniz gereken kritik güvenlik uygulamaları:
1. Her Zaman Sunucu Tarafında Doğrulama Yapın
İstemci tarafındaki doğrulama sadece kullanıcı deneyimi içindir; güvenlik açısından asla yeterli olmaz. Bir saldırgan istemci tarafı doğrulamasını kolayca atlayabilir.
// YANLIŞ: Sadece istemci doğrulamasına güvenmek
// Saldırgan doğrudan POST isteği gönderebilir!
// DOGRU: Her zaman sunucuda doğrulama yapın
"use server";
import { z } from "zod";
const yorumSemasi = z.object({
icerik: z.string().min(1).max(5000),
yaziId: z.string().uuid(),
});
export async function yorumEkle(formData: FormData) {
// Sunucu tarafı doğrulama ZORUNLU
const sonuc = yorumSemasi.safeParse({
icerik: formData.get("icerik"),
yaziId: formData.get("yaziId"),
});
if (!sonuc.success) {
return { hata: "Geçersiz veri." };
}
// Doğrulanmış veriyle devam et
const { icerik, yaziId } = sonuc.data;
// ...
}
2. Kimlik Doğrulama ve Yetkilendirme Kontrolü
Her server action kullanıcının kimliğini ve yetkisini doğrulamalıdır. Oturum bilgisine asla istemciden gelen veriye güvenerek ulaşmayın; her zaman sunucu tarafında oturum kontrolü yapın.
// app/actions/yazi-actions.ts
"use server";
import { auth } from "@/lib/auth";
import { redirect } from "next/navigation";
export async function yaziSil(yaziId: string) {
// 1. Kimlik doğrulama
const oturum = await auth();
if (!oturum?.user) {
redirect("/giris");
}
// 2. Yetkilendirme - kullanıcı bu yazının sahibi mi?
const yazi = await prisma.yazi.findUnique({
where: { id: yaziId },
select: { yazarId: true },
});
if (!yazi) {
return { hata: "Yazı bulunamadı." };
}
if (yazi.yazarId !== oturum.user.id && oturum.user.rol !== "ADMIN") {
return { hata: "Bu işlem için yetkiniz yok." };
}
// 3. Güvenli işlem
await prisma.yazi.delete({ where: { id: yaziId } });
revalidatePath("/yazilar");
return { basarili: true };
}
3. CSRF Koruması
Next.js, Server Actions için yerleşik CSRF koruması sağlıyor. Her server action çağrısı Origin başlığını kontrol eder ve yalnızca aynı kaynaktan gelen istekleri kabul eder. Ayrıca server action'ların benzersiz kimlikleri tahmin edilemez olduğundan, kör CSRF saldırılarına karşı ek bir koruma katmanı da var. Yine de next.config.js dosyasında serverActions.allowedOrigins yapılandırmasını doğru ayarladığınızdan emin olun:
// next.config.js
/** @type {import('next').NextConfig} */
const nextConfig = {
experimental: {
serverActions: {
allowedOrigins: [
"sizin-alaniniz.com",
"localhost:3000", // Geliştirme ortamı
],
bodySizeLimit: "2mb", // Gövde boyutu sınırı
},
},
};
module.exports = nextConfig;
4. Girdi Temizleme (Input Sanitization)
Kullanıcıdan gelen tüm verileri güvenilmez olarak ele alın. SQL injection, XSS ve diğer injection saldırılarına karşı girdileri temizlemek şart.
"use server";
import DOMPurify from "isomorphic-dompurify";
export async function profilGuncelle(formData: FormData) {
const oturum = await auth();
if (!oturum?.user) return { hata: "Yetkisiz erişim." };
const biyografi = formData.get("biyografi") as string;
// HTML içeriğini temizle - XSS önleme
const temizBiyografi = DOMPurify.sanitize(biyografi, {
ALLOWED_TAGS: ["b", "i", "em", "strong", "a", "p", "br"],
ALLOWED_ATTR: ["href", "target"],
});
await prisma.profil.update({
where: { kullaniciId: oturum.user.id },
data: { biyografi: temizBiyografi },
});
revalidatePath(`/profil/${oturum.user.id}`);
return { basarili: true };
}
5. Hız Sınırlama (Rate Limiting)
Server Actions kötü niyetli kullanıcılar tarafından suistimal edilebilir. Özellikle form gönderimi veya kaynak yoğun işlemler için hız sınırlama uygulamak önemli:
// lib/hiz-sinir.ts
import { headers } from "next/headers";
const istekSayaci = new Map<string, { sayi: number; sonSifirlama: number }>();
export async function hizSiniriKontrol(
sinir: number = 10,
pencereSuresiMs: number = 60000
): Promise<boolean> {
const basliklar = await headers();
const ip = basliklar.get("x-forwarded-for") ?? "bilinmeyen";
const simdi = Date.now();
const mevcut = istekSayaci.get(ip);
if (!mevcut || simdi - mevcut.sonSifirlama > pencereSuresiMs) {
istekSayaci.set(ip, { sayi: 1, sonSifirlama: simdi });
return true; // İzin ver
}
if (mevcut.sayi >= sinir) {
return false; // Engelle
}
mevcut.sayi++;
return true; // İzin ver
}
// Kullanım:
// "use server";
// export async function hassasIslem(formData: FormData) {
// const izinVerildi = await hizSiniriKontrol(5, 60000);
// if (!izinVerildi) {
// return { hata: "Çok fazla istek. Lütfen bir dakika bekleyin." };
// }
// // İşleme devam et...
// }
Üretim ortamında bellek içi bir sayaç yerine Redis gibi dağıtılmış bir cache kullanmanız çok daha iyi olur. @upstash/ratelimit gibi kütüphaneler bu iş için biçilmiş kaftan.
Önbellek Yönetimi: revalidatePath ve revalidateTag
Next.js'in App Router mimarisi oldukça agresif bir caching stratejisi kullanıyor. Server Actions ile veri mutasyonu yaptığınızda ilgili önbellekleri geçersiz kılmanız gerekir. Bunun için üç temel araç var: revalidatePath, revalidateTag ve daha yeni eklenen cacheTag / cacheLife API'leri.
revalidatePath
revalidatePath, belirli bir path'in önbelleğini geçersiz kılar. Bu yola yapılacak sonraki istek sayfayı yeniden oluşturur.
"use server";
import { revalidatePath } from "next/cache";
export async function urunGuncelle(urunId: string, formData: FormData) {
// Veritabanı güncelleme...
await prisma.urun.update({
where: { id: urunId },
data: { isim: formData.get("isim") as string },
});
// Belirli bir sayfayı geçersiz kıl
revalidatePath(`/urunler/${urunId}`);
// Ürün listesi sayfasını da geçersiz kıl
revalidatePath("/urunler");
// Layout düzeyinde geçersiz kılma
revalidatePath("/urunler", "layout");
// Tüm veriyi geçersiz kıl (dikkatli kullanın)
revalidatePath("/", "layout");
}
revalidateTag
revalidateTag etiket tabanlı önbellek geçersiz kılma sağlar. Veri çekme işlemlerinize etiketler atayarak daha hassas bir cache kontrolü elde edebilirsiniz.
// Veri çekme sırasında etiket atama
async function urunleriGetir() {
const yanit = await fetch("https://api.ornek.com/urunler", {
next: {
tags: ["urunler"], // Bu veriye "urunler" etiketi ata
revalidate: 3600, // 1 saat sonra otomatik yenile
},
});
return yanit.json();
}
async function urunDetayGetir(id: string) {
const yanit = await fetch(`https://api.ornek.com/urunler/${id}`, {
next: {
tags: ["urunler", `urun-${id}`], // Birden fazla etiket
},
});
return yanit.json();
}
// Server Action'da etiket bazlı geçersiz kılma
"use server";
import { revalidateTag } from "next/cache";
export async function urunSil(urunId: string) {
await prisma.urun.delete({ where: { id: urunId } });
// Belirli ürünün önbelleğini temizle
revalidateTag(`urun-${urunId}`);
// Tüm ürün listelerinin önbelleğini temizle
revalidateTag("urunler");
}
Ne Zaman Hangisini Kullanmalı?
- revalidatePath: Belirli bir sayfa veya rotanın tamamını yenilemek istediğinizde. Sayfa düzeyinde düşünürken en iyi seçenek — özellikle sayfanın tüm verilerinin güncellenmesi gerektiğinde ideal.
- revalidateTag: Birden fazla sayfada kullanılan belirli bir veri kümesini yenilemek istediğinizde. Veri düzeyinde düşünürken daha uygun. Mesela bir ürün güncellendiğinde hem detay sayfası, hem liste sayfası, hem de ana sayfadaki öne çıkan ürünler bölümü güncellenir.
Pratik Önbellek Stratejisi Örneği
// app/actions/blog-actions.ts
"use server";
import { revalidatePath, revalidateTag } from "next/cache";
export async function blogYazisiOlustur(
oncekiDurum: unknown,
formData: FormData
) {
// ... doğrulama ve kaydetme ...
// Yeni yazı oluşturulduğunda:
// 1. Blog listesi sayfasını yenile (yeni yazı görünsün)
revalidateTag("blog-yazilari");
// 2. Kategori sayfasını yenile
const kategori = formData.get("kategori") as string;
revalidateTag(`kategori-${kategori}`);
// 3. Ana sayfa istatistiklerini yenile
revalidateTag("site-istatistikleri");
}
export async function blogYazisiGuncelle(
yaziId: string,
formData: FormData
) {
// ... doğrulama ve güncelleme ...
// Güncelleme durumunda:
// 1. Spesifik yazı detay sayfasını yenile
revalidateTag(`yazi-${yaziId}`);
// 2. Yazı listesini yenile (başlık değişmiş olabilir)
revalidateTag("blog-yazilari");
// 3. İlgili yazı sayfasının path'ini yenile
const slug = formData.get("slug") as string;
revalidatePath(`/blog/${slug}`);
}
Veritabanı İşlemleri ile Server Actions
Server Actions, veritabanı CRUD işlemleri için mükemmel bir eşleşme. Prisma ORM ile kullanıldığında tip güvenli ve temiz bir veritabanı katmanı oluşturabilirsiniz.
Prisma Kurulumu ve Model Tanımlama
// prisma/schema.prisma
generator client {
provider = "prisma-client-js"
}
datasource db {
provider = "postgresql"
url = env("DATABASE_URL")
}
model Gorev {
id String @id @default(cuid())
baslik String
aciklama String?
durum GorevDurumu @default(BEKLEMEDE)
oncelik Int @default(0)
olusturanId String
olusturan Kullanici @relation(fields: [olusturanId], references: [id])
olusturmaZamani DateTime @default(now())
guncellemeZamani DateTime @updatedAt
}
enum GorevDurumu {
BEKLEMEDE
DEVAM_EDIYOR
TAMAMLANDI
IPTAL_EDILDI
}
Tam CRUD Server Actions
Şimdi asıl işe geliyoruz. İşte Prisma ile tam CRUD (oluşturma, okuma, güncelleme, silme) işlemlerini kapsayan server action'lar:
// app/actions/gorev-actions.ts
"use server";
import { z } from "zod";
import { prisma } from "@/lib/prisma";
import { auth } from "@/lib/auth";
import { revalidatePath } from "next/cache";
import { redirect } from "next/navigation";
// Doğrulama şemaları
const gorevOlusturmaSemasi = z.object({
baslik: z.string().min(1, "Başlık gerekli.").max(200, "Başlık çok uzun."),
aciklama: z.string().max(5000, "Açıklama çok uzun.").optional(),
oncelik: z.coerce.number().int().min(0).max(5).default(0),
});
const gorevGuncellemeSemasi = z.object({
id: z.string().cuid("Geçersiz görev kimliği."),
baslik: z.string().min(1).max(200).optional(),
aciklama: z.string().max(5000).optional(),
durum: z.enum(["BEKLEMEDE", "DEVAM_EDIYOR", "TAMAMLANDI", "IPTAL_EDILDI"]).optional(),
oncelik: z.coerce.number().int().min(0).max(5).optional(),
});
// Sonuç tipi
interface ActionSonucu {
basarili: boolean;
mesaj: string;
hatalar?: Record<string, string[]>;
}
// OLUŞTURMA
export async function gorevOlustur(
oncekiDurum: ActionSonucu,
formData: FormData
): Promise<ActionSonucu> {
const oturum = await auth();
if (!oturum?.user?.id) {
redirect("/giris");
}
const dogrulama = gorevOlusturmaSemasi.safeParse({
baslik: formData.get("baslik"),
aciklama: formData.get("aciklama"),
oncelik: formData.get("oncelik"),
});
if (!dogrulama.success) {
return {
basarili: false,
mesaj: "Doğrulama hatası.",
hatalar: dogrulama.error.flatten().fieldErrors as Record<string, string[]>,
};
}
try {
const yeniGorev = await prisma.gorev.create({
data: {
...dogrulama.data,
olusturanId: oturum.user.id,
},
});
revalidatePath("/gorevler");
return {
basarili: true,
mesaj: `"${yeniGorev.baslik}" görevi oluşturuldu.`,
};
} catch (hata) {
console.error("Görev oluşturma hatası:", hata);
return {
basarili: false,
mesaj: "Görev oluşturulurken bir hata meydana geldi.",
};
}
}
// GÜNCELLEME
export async function gorevGuncelle(
oncekiDurum: ActionSonucu,
formData: FormData
): Promise<ActionSonucu> {
const oturum = await auth();
if (!oturum?.user?.id) {
redirect("/giris");
}
const dogrulama = gorevGuncellemeSemasi.safeParse({
id: formData.get("id"),
baslik: formData.get("baslik") || undefined,
aciklama: formData.get("aciklama") || undefined,
durum: formData.get("durum") || undefined,
oncelik: formData.get("oncelik") || undefined,
});
if (!dogrulama.success) {
return {
basarili: false,
mesaj: "Doğrulama hatası.",
hatalar: dogrulama.error.flatten().fieldErrors as Record<string, string[]>,
};
}
try {
// Yetkilendirme kontrolü
const mevcutGorev = await prisma.gorev.findUnique({
where: { id: dogrulama.data.id },
select: { olusturanId: true },
});
if (!mevcutGorev) {
return { basarili: false, mesaj: "Görev bulunamadı." };
}
if (mevcutGorev.olusturanId !== oturum.user.id) {
return { basarili: false, mesaj: "Bu görevi güncelleme yetkiniz yok." };
}
const { id, ...guncellenecekVeri } = dogrulama.data;
await prisma.gorev.update({
where: { id },
data: guncellenecekVeri,
});
revalidatePath("/gorevler");
revalidatePath(`/gorevler/${id}`);
return {
basarili: true,
mesaj: "Görev başarıyla güncellendi.",
};
} catch (hata) {
console.error("Görev güncelleme hatası:", hata);
return {
basarili: false,
mesaj: "Görev güncellenirken bir hata meydana geldi.",
};
}
}
// SİLME
export async function gorevSil(gorevId: string): Promise<ActionSonucu> {
const oturum = await auth();
if (!oturum?.user?.id) {
redirect("/giris");
}
// Girdi doğrulama
const idDogrulama = z.string().cuid().safeParse(gorevId);
if (!idDogrulama.success) {
return { basarili: false, mesaj: "Geçersiz görev kimliği." };
}
try {
const gorev = await prisma.gorev.findUnique({
where: { id: gorevId },
select: { olusturanId: true },
});
if (!gorev) {
return { basarili: false, mesaj: "Görev bulunamadı." };
}
if (gorev.olusturanId !== oturum.user.id) {
return { basarili: false, mesaj: "Bu görevi silme yetkiniz yok." };
}
await prisma.gorev.delete({ where: { id: gorevId } });
revalidatePath("/gorevler");
return {
basarili: true,
mesaj: "Görev başarıyla silindi.",
};
} catch (hata) {
console.error("Görev silme hatası:", hata);
return {
basarili: false,
mesaj: "Görev silinirken bir hata meydana geldi.",
};
}
}
Silme İşlemi İçin Bileşen Örneği
// app/gorevler/gorev-sil-butonu.tsx
"use client";
import { useTransition } from "react";
import { gorevSil } from "@/app/actions/gorev-actions";
interface GorevSilButonuProps {
gorevId: string;
gorevBasligi: string;
}
export function GorevSilButonu({ gorevId, gorevBasligi }: GorevSilButonuProps) {
const [bekleniyor, gecisBaslat] = useTransition();
function silmeIslemi() {
const onay = confirm(`"${gorevBasligi}" görevini silmek istediğinize emin misiniz?`);
if (!onay) return;
gecisBaslat(async () => {
const sonuc = await gorevSil(gorevId);
if (!sonuc.basarili) {
alert(sonuc.mesaj);
}
});
}
return (
<button
onClick={silmeIslemi}
disabled={bekleniyor}
className="text-red-600 hover:text-red-800 disabled:opacity-50"
>
{bekleniyor ? "Siliniyor..." : "Sil"}
</button>
);
}
İleri Düzey Kalıplar
Server Actions'ın temellerini öğrendik, şimdi daha gelişmiş kullanım kalıplarına geçelim. Bu kalıplar üretim düzeyindeki uygulamalarda gerçekten karşılaşacağınız senaryoları ele alıyor.
Optimistik Güncellemeler (useOptimistic)
Optimistik güncellemeler, sunucu yanıtını beklemeden UI'ı anında günceller. Bu, uygulamanızın çok daha hızlı ve duyarlı hissettirmesini sağlıyor. React 19'un useOptimistic hook'u bu kalıbı oldukça kolaylaştırıyor.
// app/gorevler/gorev-listesi.tsx
"use client";
import { useOptimistic } from "react";
import { gorevDurumGuncelle } from "@/app/actions/gorev-actions";
interface Gorev {
id: string;
baslik: string;
tamamlandi: boolean;
}
interface GorevListesiProps {
gorevler: Gorev[];
}
export function GorevListesi({ gorevler }: GorevListesiProps) {
const [optimistikGorevler, optimistikGuncelle] = useOptimistic(
gorevler,
(mevcutGorevler: Gorev[], guncellenenId: string) => {
return mevcutGorevler.map((gorev) =>
gorev.id === guncellenenId
? { ...gorev, tamamlandi: !gorev.tamamlandi }
: gorev
);
}
);
async function durumDegistir(gorevId: string) {
// 1. Arayüzü hemen güncelle (optimistik)
optimistikGuncelle(gorevId);
// 2. Sunucuya isteği gönder
const sonuc = await gorevDurumGuncelle(gorevId);
// Hata durumunda React otomatik olarak önceki duruma döner
if (!sonuc.basarili) {
console.error("Durum güncelleme hatası:", sonuc.mesaj);
}
}
return (
<ul className="space-y-2">
{optimistikGorevler.map((gorev) => (
<li key={gorev.id} className="flex items-center gap-3 p-3 border rounded">
<form action={() => durumDegistir(gorev.id)}>
<button
type="submit"
className={`w-5 h-5 rounded border-2 flex items-center justify-center
${gorev.tamamlandi
? "bg-green-500 border-green-500 text-white"
: "border-gray-300"
}`}
>
{gorev.tamamlandi && "✓"}
</button>
</form>
<span className={gorev.tamamlandi ? "line-through text-gray-400" : ""}>
{gorev.baslik}
</span>
</li>
))}
</ul>
);
}
Progressif Geliştirme (Progressive Enhancement)
Bence Server Actions'ın en güçlü özelliklerinden biri bu. Form'a action prop'u olarak bir server action verildiğinde, JavaScript devre dışı olsa bile form çalışır. Erişilebilirlik ve dayanıklılık açısından büyük bir avantaj.
// app/arama/page.tsx
// Bu form JavaScript olmadan da çalışır!
import { aramaYap } from "@/app/actions/arama-actions";
export default function AramaSayfasi() {
return (
<form action={aramaYap} className="flex gap-2">
<input
type="text"
name="sorgu"
placeholder="Aramak istediğiniz terimi yazın..."
className="flex-1 border rounded px-3 py-2"
required
/>
<button
type="submit"
className="bg-blue-600 text-white px-6 py-2 rounded"
>
Ara
</button>
</form>
);
}
JavaScript yüklendiğinde React formu hydrate ederek tam istemci tarafı deneyimini sağlar. JavaScript yüklenemezse form klasik HTML form gönderimi olarak çalışmaya devam eder. Oldukça zarif bir çözüm.
Dosya Yükleme
Server Actions FormData ile çalıştığından dosya yükleme işlemleri doğal olarak destekleniyor:
// app/actions/dosya-actions.ts
"use server";
import { writeFile, mkdir } from "fs/promises";
import path from "path";
import { z } from "zod";
import { auth } from "@/lib/auth";
const IZIN_VERILEN_TIPLER = [
"image/jpeg",
"image/png",
"image/webp",
"image/avif",
];
const MAKS_BOYUT = 5 * 1024 * 1024; // 5 MB
interface DosyaYuklemeSonucu {
basarili: boolean;
mesaj: string;
dosyaYolu?: string;
}
export async function avatarYukle(
oncekiDurum: DosyaYuklemeSonucu,
formData: FormData
): Promise<DosyaYuklemeSonucu> {
const oturum = await auth();
if (!oturum?.user?.id) {
return { basarili: false, mesaj: "Oturum açmanız gerekiyor." };
}
const dosya = formData.get("avatar") as File | null;
if (!dosya || dosya.size === 0) {
return { basarili: false, mesaj: "Lütfen bir dosya seçin." };
}
// Dosya tipi kontrolü
if (!IZIN_VERILEN_TIPLER.includes(dosya.type)) {
return {
basarili: false,
mesaj: "Yalnızca JPEG, PNG, WebP ve AVIF dosyaları kabul edilir.",
};
}
// Dosya boyutu kontrolü
if (dosya.size > MAKS_BOYUT) {
return {
basarili: false,
mesaj: "Dosya boyutu 5 MB'ı geçemez.",
};
}
try {
const tampon = Buffer.from(await dosya.arrayBuffer());
const uzanti = dosya.type.split("/")[1];
const dosyaAdi = `avatar-${oturum.user.id}-${Date.now()}.${uzanti}`;
const yuklemeDizini = path.join(process.cwd(), "public", "uploads", "avatarlar");
// Dizin yoksa oluştur
await mkdir(yuklemeDizini, { recursive: true });
const dosyaYolu = path.join(yuklemeDizini, dosyaAdi);
await writeFile(dosyaYolu, tampon);
// Veritabanında profil resmini güncelle
// await prisma.kullanici.update({
// where: { id: oturum.user.id },
// data: { avatarUrl: `/uploads/avatarlar/${dosyaAdi}` },
// });
return {
basarili: true,
mesaj: "Avatar başarıyla güncellendi!",
dosyaYolu: `/uploads/avatarlar/${dosyaAdi}`,
};
} catch (hata) {
console.error("Dosya yükleme hatası:", hata);
return {
basarili: false,
mesaj: "Dosya yüklenirken bir hata oluştu.",
};
}
}
Server Action'ları Birleştirme (Composing)
Karmaşık iş süreçlerinde birden fazla server action'ı bir arada kullanmanız gerekebilir. Sipariş oluşturma gibi çok adımlı bir süreci düşünün — stok kontrolü, ödeme işleme ve stok güncelleme hep bir arada olmalı. İşte bu kalıp için temiz bir yaklaşım:
// app/actions/siparis-actions.ts
"use server";
import { prisma } from "@/lib/prisma";
import { revalidatePath } from "next/cache";
// Alt işlem: Stok kontrolü
async function stokKontrol(urunId: string, miktar: number): Promise<boolean> {
const urun = await prisma.urun.findUnique({
where: { id: urunId },
select: { stok: true },
});
return urun !== null && urun.stok >= miktar;
}
// Alt işlem: Ödeme işleme
async function odemeIsle(
tutar: number,
odemeYontemi: string
): Promise<{ basarili: boolean; islemId?: string }> {
// Ödeme servisi entegrasyonu
// const sonuc = await stripeClient.charges.create({ ... });
return { basarili: true, islemId: `odm-${Date.now()}` };
}
// Alt işlem: Stok güncelleme
async function stokGuncelle(urunId: string, miktar: number) {
await prisma.urun.update({
where: { id: urunId },
data: { stok: { decrement: miktar } },
});
}
// Ana server action: Sipariş oluştur
export async function siparisOlustur(
oncekiDurum: unknown,
formData: FormData
) {
const oturum = await auth();
if (!oturum?.user) {
return { basarili: false, mesaj: "Oturum açmanız gerekiyor." };
}
const urunId = formData.get("urunId") as string;
const miktar = Number(formData.get("miktar"));
const odemeYontemi = formData.get("odemeYontemi") as string;
// 1. Stok kontrolü
const stokYeterli = await stokKontrol(urunId, miktar);
if (!stokYeterli) {
return { basarili: false, mesaj: "Üzgünüz, yeterli stok bulunmuyor." };
}
// 2. Fiyat hesaplama
const urun = await prisma.urun.findUnique({ where: { id: urunId } });
if (!urun) {
return { basarili: false, mesaj: "Ürün bulunamadı." };
}
const toplamTutar = urun.fiyat * miktar;
// 3. Ödeme işleme
const odemeSonucu = await odemeIsle(toplamTutar, odemeYontemi);
if (!odemeSonucu.basarili) {
return { basarili: false, mesaj: "Ödeme işlemi başarısız oldu." };
}
// 4. Veritabanı transaction ile sipariş ve stok güncelleme
try {
await prisma.$transaction(async (tx) => {
// Sipariş oluştur
await tx.siparis.create({
data: {
kullaniciId: oturum.user.id,
urunId,
miktar,
toplamTutar,
odemeIslemId: odemeSonucu.islemId!,
durum: "ONAYLANDI",
},
});
// Stok güncelle
await tx.urun.update({
where: { id: urunId },
data: { stok: { decrement: miktar } },
});
});
revalidatePath("/siparisler");
revalidatePath(`/urunler/${urunId}`);
return {
basarili: true,
mesaj: "Siparişiniz başarıyla oluşturuldu!",
};
} catch (hata) {
console.error("Sipariş oluşturma hatası:", hata);
// Ödeme iadesi gerekebilir
return {
basarili: false,
mesaj: "Sipariş oluşturulurken bir hata oluştu. Ödemeniz iade edilecektir.",
};
}
}
Middleware Entegrasyonu
Server Actions'ı middleware benzeri bir yapıyla sarmalayarak loglama, hız sınırlama ve yetkilendirme gibi ortak mantığı tek bir noktada yönetebilirsiniz. Bunu projelerimde sıkça kullanıyorum ve gerçekten işleri temiz tutuyor:
// lib/action-sarmalayici.ts
import { auth } from "@/lib/auth";
import { redirect } from "next/navigation";
type ServerActionFn<TDurum> = (
durum: TDurum,
formData: FormData
) => Promise<TDurum>;
interface SarmalayiciSecenekleri {
kimlikDogrulamaGerekli?: boolean;
izinVerilenRoller?: string[];
}
export function korunmusAction<TDurum extends { basarili: boolean; mesaj: string }>(
action: ServerActionFn<TDurum>,
secenekler: SarmalayiciSecenekleri = {}
): ServerActionFn<TDurum> {
return async (durum: TDurum, formData: FormData): Promise<TDurum> => {
const baslangic = Date.now();
try {
// Kimlik doğrulama kontrolü
if (secenekler.kimlikDogrulamaGerekli) {
const oturum = await auth();
if (!oturum?.user) {
redirect("/giris");
}
// Rol kontrolü
if (
secenekler.izinVerilenRoller &&
!secenekler.izinVerilenRoller.includes(oturum.user.rol)
) {
return {
...durum,
basarili: false,
mesaj: "Bu işlem için yetkiniz yok.",
};
}
}
// Action'ı çalıştır
const sonuc = await action(durum, formData);
// Loglama
const sure = Date.now() - baslangic;
console.log(`[Action] Süre: ${sure}ms, Başarılı: ${sonuc.basarili}`);
return sonuc;
} catch (hata) {
const sure = Date.now() - baslangic;
console.error(`[Action Hatası] Süre: ${sure}ms`, hata);
return {
...durum,
basarili: false,
mesaj: "Beklenmeyen bir hata oluştu.",
};
}
};
}
// Kullanım:
// export const gorevOlustur = korunmusAction(
// async (durum, formData) => { /* ... */ },
// { kimlikDogrulamaGerekli: true, izinVerilenRoller: ["ADMIN", "EDITOR"] }
// );
Performans ve Optimizasyon
Server Actions güçlü bir araç, ama doğru kullanılmadığında performans sorunlarına yol açabilir. Dikkat etmeniz gereken birkaç kritik nokta var.
Server Actions'ı Veri Çekme İçin Kullanmayın
Bu en yaygın hatalardan biri ve bunu söylemek zorundayım. Server Actions veri mutasyonları (oluşturma, güncelleme, silme) için tasarlandı. Veri okuma işlemleri için Server Components, fetch ile caching veya React'in use hook'unu kullanmalısınız.
// YANLIŞ: Server Action ile veri çekme
"use server";
export async function urunleriGetir() {
return await prisma.urun.findMany();
}
// DOGRU: Server Component'te doğrudan veri çekme
// app/urunler/page.tsx
export default async function UrunlerSayfasi() {
// Bu bir server action DEĞİL, bir Server Component
const urunler = await prisma.urun.findMany({
orderBy: { olusturmaZamani: "desc" },
});
return (
<div>
{urunler.map((urun) => (
<UrunKarti key={urun.id} urun={urun} />
))}
</div>
);
}
Neden mi? Server Components'teki veri çekme işlemleri Next.js'in caching mekanizmasından tam olarak faydalanır. Server Actions ise her çağrıda yeni bir POST isteği oluşturur ve varsayılan olarak cache'lenmez.
Paket Boyutu Avantajı
Server Actions'ın gerçekten güzel bir avantajı var: sunucu tarafı kodu istemci JavaScript paketine dahil edilmiyor. Ağır bağımlılıklar kullanan action'lar için bu büyük bir kazanç.
// Bu kod ASLA istemci paketine dahil edilmez
"use server";
import { PDFDocument } from "pdf-lib"; // ~300KB
import sharp from "sharp"; // ~25MB (native)
import { createTransport } from "nodemailer"; // ~200KB
export async function raporOlustur(formData: FormData) {
// Tüm bu ağır kütüphaneler yalnızca sunucuda çalışır
// İstemci tarafında sadece bu fonksiyonu çağıran küçük bir referans bulunur
const pdf = await PDFDocument.create();
// ...
}
Geleneksel API route'larıyla kıyaslandığında da avantajlı, çünkü istemci tarafında fetch çağrısı, hata işleme ve state yönetimi için gereken ek kod da ortadan kalkıyor.
Streaming ve Uzun Süren İşlemler
Server Actions uzun süren işlemler için doğrudan streaming desteği sunmuyor. Bu tür senaryolarda yaklaşım şöyle olmalı: server action ile işlemi başlatıp bir iş kimliği döndürün, sonra polling veya Server-Sent Events ile durumu sorgulayın.
// app/actions/rapor-actions.ts
"use server";
import { prisma } from "@/lib/prisma";
export async function raporOlusturBaslat(formData: FormData) {
const oturum = await auth();
if (!oturum?.user) return { basarili: false, mesaj: "Yetkisiz." };
// Veritabanında iş kaydı oluştur
const is = await prisma.arkaplanIsi.create({
data: {
tip: "RAPOR_OLUSTURMA",
durum: "KUYRUKTA",
kullaniciId: oturum.user.id,
parametreler: {
baslangicTarihi: formData.get("baslangic"),
bitisTarihi: formData.get("bitis"),
},
},
});
// Arka plan işlemini tetikle (kuyruk sistemi)
// await raporKuyrugu.ekle({ isId: is.id });
return {
basarili: true,
isId: is.id,
mesaj: "Rapor oluşturma işlemi başlatıldı.",
};
}
export async function raporDurumSorgula(isId: string) {
const is = await prisma.arkaplanIsi.findUnique({
where: { id: isId },
select: { durum: true, sonucUrl: true, hata: true },
});
return is;
}
Paralel Server Action Çağrıları
Birden fazla bağımsız server action'ı paralel çağırarak toplam bekleme süresini azaltabilirsiniz. Ama bunu yaparken useTransition içinde yönetmeye dikkat etmek önemli:
// Birden fazla bağımsız güncellemeyi paralel çalıştırma
"use server";
export async function topluGuncelleme(formData: FormData) {
const oturum = await auth();
if (!oturum?.user) return { basarili: false };
// Bağımsız işlemleri paralel çalıştır
const [profilSonuc, tercihSonuc, bildirimSonuc] = await Promise.allSettled([
profilGuncelle(formData),
tercihGuncelle(formData),
bildirimAyarGuncelle(formData),
]);
// Sonuçları kontrol et
const hatalar: string[] = [];
if (profilSonuc.status === "rejected") hatalar.push("Profil güncellenemedi.");
if (tercihSonuc.status === "rejected") hatalar.push("Tercihler güncellenemedi.");
if (bildirimSonuc.status === "rejected") hatalar.push("Bildirim ayarları güncellenemedi.");
if (hatalar.length > 0) {
return {
basarili: false,
mesaj: `Bazı güncellemeler başarısız: ${hatalar.join(" ")}`,
};
}
revalidatePath("/ayarlar");
return { basarili: true, mesaj: "Tüm ayarlar güncellendi." };
}
Sonuç
Next.js Server Actions, modern full-stack React uygulamalarının temel yapı taşlarından biri haline geldi. Bu rehberde ele aldığımız konuları kısaca özetleyelim:
- Temel kavramlar: Server Actions,
"use server"direktifi ile işaretlenen ve her zaman sunucuda çalışan asenkron fonksiyonlar. Arka planda HTTP POST istekleri olarak çalışıyor ve yerleşik güvenlik mekanizmalarına sahip. - Form yönetimi:
useActionStatehook'u form durumunu, loading göstergelerini ve hata mesajlarını yönetmek için güçlü ve ergonomik bir API sunuyor. - Veri doğrulama: Zod ile sunucu tarafı doğrulama hem tip güvenliği hem de runtime güvenliği sağlıyor.
safeParseveflattenkalıpları alan bazında hata yönetimini kolaylaştırıyor. - Güvenlik: Her server action bir açık API endpoint'i. Kimlik doğrulama, yetkilendirme, girdi doğrulama, sanitization ve rate limiting her zaman uygulanmalı.
- Önbellek yönetimi:
revalidatePathsayfa düzeyinde,revalidateTagveri düzeyinde cache invalidation sağlıyor. İkisini birlikte kullanarak hassas bir cache stratejisi oluşturabilirsiniz. - Veritabanı işlemleri: Prisma ile birlikte tip güvenli CRUD işlemleri için mükemmel bir altyapı. Transaction'lar ile atomik işlemler garanti altında.
- İleri düzey kalıplar: Optimistik güncellemeler, progressive enhancement, dosya yükleme ve middleware benzeri sarmalayıcılar — hepsi üretim kalitesinde uygulamalar için vazgeçilmez.
- Performans: Server Actions veri mutasyonları için kullanılmalı, veri çekme için değil. Sunucu kodunun istemci paketine dahil olmaması ciddi bir bundle size avantajı.
Server Actions; API route yazma, istemci tarafı fetch mantığı kurma ve state yönetimi katmanları oluşturma gibi tekrarlayan işleri ortadan kaldırarak geliştiricilerin asıl iş mantığına odaklanmasını sağlıyor. Next.js 15 ve sonrasıyla bu teknoloji olgunlaşmaya devam ediyor ve React ekosisteminin geleceğinde merkezi bir rol oynaması bekleniyor.
Bu rehberdeki kalıpları kendi projelerinize uygularken güvenliği ön planda tutmayı, doğrulama katmanlarını atlamadan uygulamayı ve performans etkilerini göz önünde bulundurmayı unutmayın. Server Actions'ın asıl gücü, basitliği ile güvenli ve ölçeklenebilir uygulamalar inşa etme kapasitesinin birleşiminde yatıyor.