İçeriğe geç
nextjs

Next.js Server Actions: Form İşleme Rehberi

Tarık Tunç
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');
}
İpucu: Server Actions'da 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');
}
Uyarı: Server Actions, public API endpoint'leri olarak çalışır. Her Server Action'da mutlaka authentication ve authorization kontrolü yapın. Kullanıcıdan gelen tüm verileri validate edin. Server Action'ların istemci tarafından doğrudan çağrılabileceğini unutmayın.

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.

Kaynaklar

İlgili Yazılar