Next.js Server Actions: Form İşleme Rehberi

Next.js Server Actions: Form İşleme Rehberi
Next.js Server Actions, sunucu tarafında çalışan fonksiyonları doğrudan bileşenlerinizden çağırmanıza olanak tanır. API route oluşturmadan form gönderimi, veri mutasyonu ve sunucu işlemleri gerçekleştirebilirsiniz. Bu yazıda Server Actions'ın çalışma prensibini, form işleme pattern'lerini ve production-ready uygulamalar için en iyi pratikleri inceleyeceğiz.
Server Actions Nedir?
Server Actions, "use server" direktifi ile işaretlenen asenkron fonksiyonlardır. Bu fonksiyonlar her zaman sunucuda çalışır ve istemciden güvenli bir şekilde çağrılabilir. Next.js, arka planda bir POST endpoint oluşturur.
// app/actions.js
'use server';
export async function createUser(formData) {
const name = formData.get('name');
const email = formData.get('email');
// Doğrudan veritabanına erişim — güvenli, sunucuda çalışır
const user = await db.user.create({
data: { name, email },
});
return user;
}
Form'larda Kullanım
Server Actions, HTML form elemanlarının action attribute'ına doğrudan geçirilebilir:
// app/signup/page.js
import { createUser } from '@/app/actions';
export default function SignupPage() {
return (
<form action={createUser}>
<label htmlFor="name">İsim</label>
<input id="name" name="name" type="text" required />
<label htmlFor="email">E-posta</label>
<input id="email" name="email" type="email" required />
<button type="submit">Kayıt Ol</button>
</form>
);
}
useActionState ile Form State Yönetimi
useActionState, form action'ının sonucunu ve pending durumunu yönetir:
'use client';
import { useActionState } from 'react';
import { createUser } from '@/app/actions';
export function SignupForm() {
const [state, formAction, isPending] = useActionState(
async (prevState, formData) => {
const result = await createUser(formData);
return result;
},
{ message: '', errors: {} }
);
return (
<form action={formAction}>
<div>
<input name="name" type="text" placeholder="İsim" />
{state.errors?.name && (
<p className="error">{state.errors.name}</p>
)}
</div>
<div>
<input name="email" type="email" placeholder="E-posta" />
{state.errors?.email && (
<p className="error">{state.errors.email}</p>
)}
</div>
{state.message && <p className="success">{state.message}</p>}
<button type="submit" disabled={isPending}>
{isPending ? 'Kaydediliyor...' : 'Kayıt Ol'}
</button>
</form>
);
}
Zod ile Validasyon
Server Actions'da zod kullanarak güçlü sunucu taraflı validasyon yapabilirsiniz:
'use server';
import { z } from 'zod';
import { revalidatePath } from 'next/cache';
const CreatePostSchema = z.object({
title: z.string().min(3, 'Başlık en az 3 karakter olmalı').max(100),
content: z.string().min(10, 'İçerik en az 10 karakter olmalı'),
category: z.enum(['tech', 'design', 'business'], {
errorMap: () => ({ message: 'Geçerli bir kategori seçin' }),
}),
});
export async function createPost(prevState, formData) {
const rawData = {
title: formData.get('title'),
content: formData.get('content'),
category: formData.get('category'),
};
// Validasyon
const validated = CreatePostSchema.safeParse(rawData);
if (!validated.success) {
return {
success: false,
errors: validated.error.flatten().fieldErrors,
message: 'Lütfen formu kontrol edin.',
};
}
try {
await db.post.create({ data: validated.data });
revalidatePath('/posts');
return { success: true, message: 'Yazı oluşturuldu!', errors: {} };
} catch (error) {
return {
success: false,
message: 'Bir hata oluştu. Lütfen tekrar deneyin.',
errors: {},
};
}
}
useFormStatus ile Loading State
'use client';
import { useFormStatus } from 'react-dom';
function SubmitButton({ children }) {
const { pending } = useFormStatus();
return (
<button type="submit" disabled={pending} className="btn">
{pending ? (
<>
<span className="spinner" /> Gönderiliyor...
</>
) : (
children
)}
</button>
);
}
// Kullanım
<form action={createPost}>
<input name="title" />
<SubmitButton>Yazıyı Yayınla</SubmitButton>
</form>
Optimistic Updates
'use client';
import { useOptimistic } from 'react';
import { toggleLike } from '@/app/actions';
export function LikeButton({ postId, initialLiked, initialCount }) {
const [optimistic, setOptimistic] = useOptimistic(
{ liked: initialLiked, count: initialCount },
(current, newLiked) => ({
liked: newLiked,
count: newLiked ? current.count + 1 : current.count - 1,
})
);
async function handleLike() {
setOptimistic(!optimistic.liked);
await toggleLike(postId);
}
return (
<form action={handleLike}>
<button type="submit">
{optimistic.liked ? '❤️' : '🤍'} {optimistic.count}
</button>
</form>
);
}
Dosya Yükleme
'use server';
import { writeFile } from 'fs/promises';
import path from 'path';
export async function uploadFile(formData) {
const file = formData.get('file');
if (!file || file.size === 0) {
return { error: 'Dosya seçilmedi' };
}
// Dosya boyutu kontrolü (5MB)
if (file.size > 5 * 1024 * 1024) {
return { error: 'Dosya boyutu 5MB\'dan büyük olamaz' };
}
// Dosya türü kontrolü
const allowedTypes = ['image/jpeg', 'image/png', 'image/webp'];
if (!allowedTypes.includes(file.type)) {
return { error: 'Sadece JPEG, PNG ve WebP dosyaları yüklenebilir' };
}
const bytes = await file.arrayBuffer();
const buffer = Buffer.from(bytes);
const fileName = `${Date.now()}-${file.name}`;
const filePath = path.join(process.cwd(), 'public/uploads', fileName);
await writeFile(filePath, buffer);
return { success: true, path: `/uploads/${fileName}` };
}
'use client';
import { uploadFile } from '@/app/actions';
import { useActionState } from 'react';
export function FileUploadForm() {
const [state, action, isPending] = useActionState(
async (prev, formData) => await uploadFile(formData),
null
);
return (
<form action={action}>
<input type="file" name="file" accept="image/*" required />
<button type="submit" disabled={isPending}>
{isPending ? 'Yükleniyor...' : 'Yükle'}
</button>
{state?.error && <p className="error">{state.error}</p>}
{state?.success && <p className="success">Yüklendi: {state.path}</p>}
</form>
);
}
Revalidation Stratejileri
'use server';
import { revalidatePath, revalidateTag } from 'next/cache';
import { redirect } from 'next/navigation';
export async function updateProfile(formData) {
const name = formData.get('name');
await db.user.update({
where: { id: currentUserId },
data: { name },
});
// Path bazlı revalidation
revalidatePath('/profile');
// Tag bazlı revalidation
revalidateTag('user-data');
// İsteğe bağlı: Başka sayfaya yönlendir
redirect('/profile');
}
redirect() çağrısı try/catch bloğunun dışında yapılmalıdır. redirect() bir error fırlatarak çalışır ve try/catch içinde yakalanırsa beklendiği gibi çalışmaz.
Güvenlik Önlemleri
'use server';
import { auth } from '@/lib/auth';
export async function deletePost(postId) {
// 1. Authentication kontrolü
const session = await auth();
if (!session) {
throw new Error('Oturum açmanız gerekli');
}
// 2. Authorization kontrolü
const post = await db.post.findUnique({ where: { id: postId } });
if (post.authorId !== session.user.id) {
throw new Error('Bu işlem için yetkiniz yok');
}
// 3. Input validasyonu
if (typeof postId !== 'string' || !postId.match(/^[a-zA-Z0-9]+$/)) {
throw new Error('Geçersiz post ID');
}
await db.post.delete({ where: { id: postId } });
revalidatePath('/posts');
}
Non-Form Kullanım
'use client';
import { useTransition } from 'react';
import { deleteComment } from '@/app/actions';
function DeleteButton({ commentId }) {
const [isPending, startTransition] = useTransition();
const handleDelete = () => {
startTransition(async () => {
await deleteComment(commentId);
});
};
return (
<button onClick={handleDelete} disabled={isPending}>
{isPending ? 'Siliniyor...' : 'Sil'}
</button>
);
}
Sonuç
Next.js Server Actions, form işleme ve veri mutasyonunu radikal biçimde basitleştiren güçlü bir özelliktir. API route katmanını ortadan kaldırarak doğrudan veritabanı erişimi, dosya işlemleri ve iş mantığı çalıştırmanıza olanak tanır. Zod ile validasyon, useActionState ile state yönetimi, useOptimistic ile optimistic UI ve revalidation stratejileri ile production-ready uygulamalar oluşturabilirsiniz. Güvenlik kontrollerini (auth, authorization, input validation) her zaman ekleyin.