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:**

1. Server Action ditandai `"use server"` dan dipanggil langsung dari form, tanpa perlu `app/api/.../route.ts` terpisah untuk kasus paling umum.
2. Validasi HARUS diulang di server dengan skema yang sama seperti client (mis. zod), karena Server Action pada dasarnya endpoint HTTP publik.
3. Honeypot field tersembunyi lewat CSS adalah cara sederhana menyaring bot tanpa CAPTCHA yang mengganggu pengguna asli.
4. `useActionState` adalah pola paling umum untuk progressive enhancement, tapi bukan satu-satunya cara memanggil Server Action dari form.
5. 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](/blog/server-actions-nextjs-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`](https://react.dev/reference/react/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](https://nextjs.org/docs/app/guides/data-security),
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.

> "By default, when a Server Action is created and exported, it is
> reachable via a direct POST request, not just through your
> application's UI. This means, even if a Server Action or utility
> function is not imported elsewhere in your code, it can still be
> called externally." ([Next.js documentation, Data Security](https://nextjs.org/docs/app/guides/data-security))

## 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:

```ts
// 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:

```ts
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.

| Aspek | `useActionState` + `<form action>` | Panggil manual dari library form (mis. react-hook-form) |
| --- | --- | --- |
| Butuh JavaScript untuk submit? | Tidak — tetap jalan sebelum JS termuat | Ya — submit sepenuhnya bergantung event handler client |
| Kontrol UI error per field | Dasar, lewat objek hasil action | Lebih detail (pesan error custom, toast, dsb) |
| Setup awal | Lebih sedikit boilerplate | Perlu integrasi resolver + state form tambahan |
| Cocok untuk | Form kontak sederhana yang mengutamakan progressive enhancement | Form kompleks yang butuh validasi/UI client granular |

## 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:

```ts
import { Resend } from "resend";

const resend = new Resend(process.env.RESEND_API_KEY);

const { error } = await resend.emails.send({
  from: "onboarding@resend.dev",
  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, Validasi Ulang, dan Honeypot sebagai Inti Form Kontak Aman

- Server Action ditandai `"use server"` dipanggil langsung dari form, tanpa perlu `app/api/.../route.ts` terpisah untuk kasus paling umum.
- Validasi HARUS diulang di server dengan skema yang sama (mis. zod), karena [Server Action pada dasarnya endpoint HTTP publik](https://nextjs.org/docs/app/guides/data-security).
- Honeypot field tersembunyi lewat CSS adalah cara sederhana menyaring bot tanpa mengganggu pengguna asli dengan CAPTCHA.
- `useActionState` cocok 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](/blog/server-actions-nextjs-untuk-pemula).
- Butuh bantuan implementasi form kontak atau fitur web lain di situs sendiri? Lihat [layanan web development](/layanan) yang ditawarkan.