Server Action & Pipa Penanganan Error
Fungsi mutasi server yang dipanggil langsung dari komponen klien dengan alur pemeriksaan izin, validasi Zod, dan pembungkus error terstruktur.
Membaca sekitar 2 menit
Fungsi mutasi server yang dipanggil langsung dari komponen klien dengan alur pemeriksaan izin, validasi Zod, dan pembungkus error terstruktur.
Semua operasi tambah, ubah, dan hapus melewati pembungkus handleAction yang meneruskan sinyal redirect Next.js dan mengubah error validasi menjadi peta fieldErrors.
Action terlihat seperti fungsi lokal biasa, padahal pintunya terbuka sebagai endpoint HTTP POST yang wajib dijaga sejak baris pertama.
Masalahnya dulu
Di User Role Management, setiap tombol Tambah, Ubah, dan Hapus harus berujung pada perubahan data di server. Cara lama mengharuskan kita membuat endpoint API satu per satu, memanggilnya dengan fetch, lalu mengurus status loading dan pemetaan error sendiri. Dengan Server Action di Next.js, fungsi server memang bisa dipanggil langsung dari form, namun muncul dua bahaya baru: lupa mengecek izin karena mengira tombolnya sudah disembunyikan di UI, serta tertelannya sinyal redirect() milik Next.js saat kita membungkus kode dengan try/catch.
Bagaimana pipa handleAction bekerja
Agar tidak menulis blok try/catch berulang-ulang di puluhan action, saya membuat satu pembungkus terpusat bernama handleAction di common/utils/handle-action.ts. Di dalam setiap action, urutan langkahnya dikunci secara disiplin: cek izin lewat requirePermission, validasi payload dengan parseInput berbasis Zod, panggil fungsi service, segarkan cache rute lewat revalidatePath, dan kembalikan objek hasil terstruktur { ok, data, message, fieldErrors }.
Terlihat seperti fungsi lokal di dalam kode, padahal ia adalah pintu HTTP POST yang terbuka untuk siapa saja.
Implementasi di ngodingan ini
Berikut adalah perpaduan antara pembungkus handleAction di common/utils/handle-action.ts dan penggunaannya yang ringkas di modules/dashboard/item/item.actions.ts:
// common/utils/handle-action.ts
export async function handleAction<T>(
fn: () => Promise<T>,
successMessage?: string
): Promise<ActionResult<T>> {
try {
const data = await fn();
return { ok: true, data, message: successMessage };
} catch (e) {
unstable_rethrow(e); // biarkan redirect()/notFound() internal Next.js lewat
if (e instanceof ValidationError) {
return { ok: false, message: e.message, fieldErrors: e.fieldErrors };
}
if (e instanceof AppError) {
return {
ok: false,
message: e.message,
fieldErrors: e.field ? { [e.field]: e.message } : undefined,
};
}
console.error("[action]", e);
return { ok: false, message: "Terjadi kesalahan pada server" };
}
}
// modules/dashboard/item/item.actions.ts
export async function updateItemAction(id: string, input: unknown) {
return handleAction(async () => {
await requirePermission("item", "edit");
const uuid = parseInput(uuidSchema, id);
const data = parseInput(itemSchema, input);
await updateItem(uuid, data);
revalidatePath("/dashboard/items");
}, "Data Item berhasil diperbarui");
}Jebakan dan detail yang harus dijaga
- Wajib memanggil unstable_rethrow(e) di baris pertama catch: Di Next.js, fungsi redirect() dan notFound() bekerja dengan cara melempar error khusus. Tanpa unstable_rethrow, redirect akan tertangkap sebagai error server biasa.