Form kontak di Next.js App Router dibuat dengan memanggil Server Action ("use server") langsung dari handler submit form, bukan lewat endpoint API terpisah. Data divalidasi ulang dengan skema yang sama di server (misalnya zod), dilengkapi honeypot anti-spam dan rate limiting sederhana, sebelum email benar-benar dikirim lewat layanan seperti Resend.
Ringkasan cepat:
- Server Action ditandai
"use server"dan dipanggil langsung dari form, tanpa perluapp/api/.../route.tsterpisah untuk kasus paling umum. - Validasi HARUS diulang di server dengan skema yang sama seperti client (mis. zod), karena Server Action pada dasarnya endpoint HTTP publik.
- Honeypot field tersembunyi lewat CSS adalah cara sederhana menyaring bot tanpa CAPTCHA yang mengganggu pengguna asli.
useActionStateadalah pola paling umum untuk progressive enhancement, tapi bukan satu-satunya cara memanggil Server Action dari form.- Pengiriman email sebaiknya lewat layanan pihak ketiga (mis. Resend) dari dalam Server Action, bukan SMTP manual di server sendiri.
Apa Itu Server Actions untuk Form Kontak?
Server Actions adalah fungsi asinkron yang berjalan sepenuhnya di server,
ditandai directive "use server", dan bisa dipanggil langsung dari
komponen React tanpa membuat endpoint API terpisah untuk setiap aksi.
Konsep dasarnya sudah dibahas di panduan Server Actions Next.js untuk pemula;
artikel ini fokus pada penerapannya khusus untuk form kontak, termasuk
validasi ulang, anti-spam, dan pengiriman email.
Pola paling umum menggabungkan Server Action dengan hook useActionState
React, yang menyatukan state hasil, status pending, dan argumen sebelumnya
dalam satu hook yang terhubung ke atribut action elemen <form>.
"useActionState is a React Hook that lets you update state with side effects using Actions." (React documentation,
useActionState)
Keuntungan pola <form action={...}> plus useActionState adalah
progressive enhancement murni: form tetap bisa disubmit dan diproses
server meski JavaScript belum selesai dimuat di browser pengguna,
sama seperti perilaku dasar HTML form.
Kenapa Server Action Dianggap Endpoint Publik?
Menurut dokumentasi resmi Next.js soal keamanan data, setiap Server Action yang diekspor otomatis menjadi endpoint HTTP publik begitu referensinya ada di bundle client, dan harus diperlakukan dengan asumsi keamanan yang sama seperti endpoint API biasa — termasuk validasi ulang input dan pengecekan otorisasi di dalam fungsi itu sendiri, bukan hanya mengandalkan validasi di sisi client.
Bagaimana Cara Membuat Server Action untuk Form Kontak?
Buat file terpisah dengan directive "use server" di baris pertama,
lalu ekspor fungsi async yang menerima data form dan mengembalikan hasil
sukses/gagal:
// app/kontak/actions.ts
"use server";
import { z } from "zod";
const contactFormSchema = z.object({
name: z.string().trim().min(2, "Nama minimal 2 karakter."),
email: z.string().trim().email("Format email tidak valid."),
subject: z.string().trim().min(3, "Subjek minimal 3 karakter."),
message: z.string().trim().min(10, "Pesan minimal 10 karakter."),
});
export interface ContactActionResult {
success: boolean;
message: string;
}
export async function submitContactForm(
payload: unknown
): Promise<ContactActionResult> {
// Server tidak pernah percaya data dari client — validasi ulang di sini
// dengan skema yang sama seperti di form.
const parsed = contactFormSchema.safeParse(payload);
if (!parsed.success) {
return {
success: false,
message: "Data yang dikirim tidak valid. Periksa kembali isian form.",
};
}
// Kirim email di sini (lihat bagian pengiriman email di bawah)...
return { success: true, message: "Pesan berhasil dikirim." };
}Skema zod di atas sebaiknya diletakkan di modul terpisah yang di-import
bersama oleh client (react-hook-form/useActionState) dan Server Action,
supaya aturan validasi tidak pernah tidak sinkron antara dua sisi.
Kenapa Perlu Validasi Ulang di Server, Bukan Cukup di Client?
Validasi client (react-hook-form dengan resolver zod, atau atribut HTML
required) hanya mempercepat feedback ke pengguna asli sebelum request
terkirim. Karena Server Action pada dasarnya endpoint HTTP publik (lihat
bagian sebelumnya), siapa pun yang tahu referensinya bisa memanggilnya
langsung dengan payload sembarang, melewati form dan validasi client sama
sekali — jadi validasi di dalam Server Action itu sendiri adalah lapisan
yang benar-benar menentukan data mana yang diproses lebih lanjut.
Bagaimana Cara Menangani Spam Tanpa CAPTCHA?
Pola honeypot menambahkan satu field ekstra ke form yang disembunyikan
secara visual lewat CSS (posisi absolut di luar viewport, bukan
display:none yang bisa membuat sebagian screen reader ikut
mengabaikannya) sehingga hanya bot pengisi form otomatis yang biasanya
mengisinya:
const { name, email, subject, message, website } = parsed.data;
// Honeypot: field ini seharusnya selalu kosong untuk pengguna asli.
if (website) {
// Balas sukses palsu tanpa mengirim email, supaya bot tidak tahu
// ia terdeteksi.
return { success: true, message: "Pesan berhasil dikirim." };
}Tambahkan juga rate limiting sederhana (mis. membatasi jumlah submission per rentang waktu tertentu dari satu proses server) untuk menahan percobaan spam beruntun, meski untuk trafik tinggi lintas banyak instance server, penyimpanan bersama seperti Redis lebih tahan lama dibanding variabel in-memory yang hilang tiap restart.
Kapan Sebaiknya Pakai useActionState, dan Kapan Tidak?
useActionState lewat atribut action pada <form> adalah pola yang
paling direkomendasikan karena progressive enhancement: form tetap
terkirim dan diproses server walau JavaScript belum termuat di browser.
Tapi Server Action tetap berupa fungsi async biasa, jadi bisa juga
dipanggil manual di dalam handler submit library form pihak ketiga
(mis. react-hook-form) ketika proyek butuh kontrol validasi client dan
UI status yang lebih detail (pesan error per field yang kompleks, toast
notification kustom) dibanding yang disediakan useActionState secara
bawaan — dengan konsekuensi form tidak lagi berfungsi tanpa JavaScript,
karena submit-nya sepenuhnya bergantung pada event handler client.
Trade-off ini bukan salah satu benar dan satunya salah, murni soal prioritas: progressive enhancement penuh versus kontrol UI form yang lebih detail dengan bantuan library form.
Bagaimana Cara Mengirim Email dari Server Action?
Setelah validasi dan pengecekan honeypot lolos, panggil layanan pengirim email pihak ketiga (mis. Resend) langsung di dalam Server Action:
import { Resend } from "resend";
const resend = new Resend(process.env.RESEND_API_KEY);
const { error } = await resend.emails.send({
from: "[email protected]",
to: process.env.CONTACT_TO_EMAIL!,
replyTo: email,
subject: `[Kontak Website] ${subject}`,
text: `Nama: ${name}\nEmail: ${email}\n\nPesan:\n${message}`,
});
if (error) {
return { success: false, message: "Gagal mengirim pesan." };
}API key disimpan sebagai environment variable server-side, tidak pernah dikirim ke bundle client karena seluruh kode Server Action memang hanya dieksekusi di server. Selama pengembangan lokal sebelum kredensial layanan email diset, Server Action bisa mengembalikan sukses sambil mencatat payload ke log server saja, supaya alur UI tetap bisa diuji tanpa kredensial nyata.
Ringkasan
- Server Action ditandai
"use server"dipanggil langsung dari form, tanpa perluapp/api/.../route.tsterpisah untuk kasus paling umum. - Validasi HARUS diulang di server dengan skema yang sama (mis. zod), karena Server Action pada dasarnya endpoint HTTP publik.
- Honeypot field tersembunyi lewat CSS adalah cara sederhana menyaring bot tanpa mengganggu pengguna asli dengan CAPTCHA.
useActionStatecocok untuk progressive enhancement penuh; memanggil Server Action manual dari library form pihak ketiga cocok kalau butuh kontrol UI validasi yang lebih detail, dengan trade-off form bergantung penuh pada JavaScript.- Konsep dasar Server Actions (bukan khusus form kontak) dibahas lebih lengkap di panduan Server Actions Next.js untuk pemula.
- Butuh bantuan implementasi form kontak atau fitur web lain di situs sendiri? Lihat layanan web development yang ditawarkan.
