Form Action vs API Route
Form sunucuya gidebiliyorsa action yeter

Yorum kutusunu onSubmit + fetch('/api/comments') ile bağladım. DevTools'ta JS'i kapatınca buton bir şey yapmadı. Form vardı, action yoktu. Tarayıcının doğal gönderimi ölmüştü çünkü ben onu engellemiştim. Server action'ı formun action'ına yazınca JS açıkken de kapalıyken de aynı mutasyon çalıştı.
Formun kendi dili
import { redirect } from "next/navigation";
import { revalidatePath } from "next/cache";
import { cookies } from "next/headers";
export async function createComment(formData: FormData) {
const articleId = String(formData.get("articleId") ?? "");
const body = String(formData.get("body") ?? "").trim();
if (!articleId || body.length < 2) {
throw new Error("invalid-comment");
}
const jar = await cookies();
const session = jar.get("session")?.value;
if (!session) redirect("/login");
await saveComment({ articleId, body, session });
revalidatePath(`/blog/${articleId}`);
redirect(`/blog/${articleId}#comments`);
}
import { createComment } from "./actions";
export function CommentForm({ articleId }: { articleId: string }) {
return (
);
}
FormData alan adlarıyla gelir. Client şema doğrulaması bonus, sunucu doğrulaması kapı. redirect ve revalidatePath action içinde durur; fetch + router.refresh dansı yok. useFormStatus ile butonu pending yapmak istersen küçük bir client düğme yeter, formu client yapmak değil.
Route handler'ın kaldığı yer
Webhook imza ister, raw body ister, GET ile dosya ister, mobil istemci JSON konuşur. Bunlar form değil. /api/stripe/webhook bir route handler. /api/export.csv bir route handler. Yorum kutusu değil. 'Tek stil olsun' diye her mutasyonu API'ye taşımak, tarayıcının formunu elinden almak.
Action URL'leri framework üretir. Dış sisteme kontrat vermezsin. Mobil uygulama aynı yorumu atacaksa o zaman genel bir POST kapısı açılır; form da o kapıyı çağırabilir veya action içeride aynı saveComment'i kullanır. Paylaşılan olan route değil, fonksiyon. Route, kontrat gerektiğinde durur.
Seçim cümlesi: gönderen bir HTML form mu ve cevap bir sonraki sayfa mı? Action. Gönderen bir dış makine mi veya cevap özel bir içerik türü mü? Route. İkisini de 'backend' diye tek dosyada toplamak, kapıyı tekrar kaybettiriyor.
Hata yüzeyi
Action throw edince çerçeve error.tsx'e düşebilir. Formun yanında alan hatası istiyorsam useActionState ile düz nesne dönerim, throw etmem. 400'ü exception sanmak, on birinci yazıdaki beyaz sayfayı yorum kutusuna taşır. Route handler'da ise status kodu doğal durur: 400 JSON, 204 boş. Dış istemci bunu bekler. Form beklemez; form ya yeniden boyanır ya yönlenir.
"use client";
import { useActionState } from "react";
import { createComment } from "./actions";
export function CommentForm({ articleId }: { articleId: string }) {
const [state, action, pending] = useActionState(createComment, {
error: "",
});
return (
);
}
Bu varyant küçük bir client form. JS kapalıysa action yine çalışır, state görünmez; redirect'li sade form o yüzden hâlâ duruyor. Client'ı hata mesajı için ekliyorum, fetch için değil. API route'a dönmek, mesaj için değil, kontrat için.
Idempotency
Çift tıklama action'ı iki kez koşturur. pending ile butonu kilitlemek client'ta var, JS yoksa yok. Sunucuda aynı gövdeye kısa süreli bir anahtar koymak, route handler'daki Idempotency-Key alışkanlığının küçük hali. Bunu her yoruma koymuyorum; ödemeye koyuyorum. Ödeme zaten route veya action değil, ayrı bir kapı. Sınır yine aynı: tarayıcı formu action, makine kontratı route.
Dosya yükleme
multipart form action ile de gider. input type=file, FormData, sunucuda dosyayı oku. Progress bar istiyorsan client fetch veya xhr gerekir; o zaman route handler daha doğal durur çünkü yüzde raporlarsın. Progress yoksa action yeter. 'Yükleme var diye API' cümlesi, progress yokken gereksiz hop ekler.
Büyük dosyada action timeout'u framework ve host limitine çarpar. Orada da route veya doğrudan imzalı upload. Form yine metadata'yı action ile gönderir, dosya başka kapıdan gider. Karıştırmak, dokuzuncu yazıyı 'her şey action' ezberine çevirir. Ezber, birinci haftanın fetch ezberinin kuzeni.
Gizli alanlar ve yetki
hidden articleId güvenilmez. Action içinde oturum ve yetki tekrar bakılır. Client'ın gönderdiği id, yalnızca ipucu. Route handler'da da aynı. Fark, formun tarayıcıda görünür olması değil, sunucunun kör güvenmemesi. Action 'sunucuda ya' diye id'yi doğrulamamayı mazur göstermez.
redirect hedefi de kullanıcı girdisinden üretilmez. articleId'yi kendi sorgumla doğrulayıp bildiğim slug'a yönlendiriyorum. open redirect, action'ın sessiz kuzeni. API route'da Location header'ı için de aynı kural. Kapı değişir, güvenmezlik değişmez. Dokuzuncu yazı kapıyı seçer; kilidi seçmez, kilit her kapıda durur.
GET action yok
Mutasyonu GET'e bağlamak, önizleme botunun yorum yazması demek. Action POST gibi durur. Route handler'da da yorumu GET'e koymuyorum. Okuma GET, yazma POST/PATCH/DELETE. Form bunu doğal yapar. fetch ile API'ye GET atıp yazmak, dokuzuncu yazının reddettiği şeyin kaba hali. Kapı seçimi, metodu da seçer.
Toplu işlem, checkbox listesi, tek action. Dış sistem aynı listeyi JSON bekliyorsa route. İki istemci, iki kapı, tek saveComment. Bu tekrarı sevmiyorum diye her şeyi route'a yığmak, tarayıcı formunu tekrar öldürür. JS kapanınca yorum yine gitsin istiyorum. İstiyorsam action durur. İstemiyorsam zaten bu yazıyı yazmam.
Content-Type
Action FormData konuşur. Route JSON konuşur. İkisini tek handler'da 'ikisini de kabul et' diye birleştirmek, kapıyı tekrar bulanıklaştırır. Mobil JSON atacaksa route. Form tarayıcıdan gelecekse action. Birleştirme, dokuzuncu yazının kaçındığı 'tek stil'. Tek stil, tek istemci varsayar. İstemci ikiyse kapı iki.
Yorumlar
Yorumlar (0)
Yorumlar üyelere açık. Üye ol · Giriş yap