Pada arsitektur Next.js lama, pola yang umum untuk menangani form submission adalah: komponen frontend mengirim request ke API Route, lalu API Route melakukan validasi, menyimpan data, dan mengembalikan respons. Pendekatan itu tetap valid, tetapi pada App Router, Next.js memperkenalkan cara yang lebih terintegrasi melalui Server Actions.

Dengan Server Actions, sebuah form dapat langsung memanggil fungsi yang dijalankan di server tanpa harus membuat endpoint API terpisah untuk kasus-kasus sederhana seperti submit form, mutasi data internal, atau redirect setelah proses selesai. Hasilnya, alur data menjadi lebih ringkas: UI dan aksi server berada lebih dekat, boilerplate berkurang, dan logika mutasi bisa ditulis dengan lebih natural di dalam ekosistem React dan App Router.

Artikel ini membahas konsep Server Actions di Next.js 16, cara kerja use server, implementasi form submission, validasi input di server, simulasi penyimpanan data sederhana, redirect setelah submit, serta kapan tetap lebih tepat menggunakan API Routes.

Perubahan pola data handling: dari API Routes ke Server Actions

Pada pola tradisional, alurnya biasanya seperti ini:

  1. Komponen form di browser mengirim fetch('/api/...') dengan method POST.
  2. API Route menerima payload.
  3. API Route melakukan validasi.
  4. API Route memanggil database atau service lain.
  5. API Route mengembalikan JSON.
  6. Client memproses hasil lalu melakukan navigasi atau menampilkan error.

Di App Router, Next.js memungkinkan alur yang lebih langsung:

  1. Komponen form mendefinisikan atribut action yang mengarah ke Server Action.
  2. Saat form disubmit, Next.js mengirim data form ke fungsi server tersebut.
  3. Validasi dan mutasi dilakukan di server.
  4. Server Action dapat mengembalikan state error, memicu revalidasi cache, atau melakukan redirect().

Perbedaan pentingnya bukan hanya soal sintaks yang lebih singkat. Server Actions mengubah cara kita memikirkan mutasi data: bukan lagi selalu “buat endpoint lalu panggil dengan fetch”, tetapi “deklarasikan aksi server yang memang dipakai oleh komponen ini”. Untuk use case internal aplikasi, ini sering kali lebih sederhana dan lebih mudah dirawat.

Konsep Server Actions pada Next.js 16

Server Actions adalah fungsi yang dijalankan di server dan dapat dipanggil langsung dari komponen React, terutama dari form. Fungsi ini cocok untuk operasi yang melibatkan mutasi state backend, misalnya:

  • membuat data baru,
  • memperbarui record,
  • menghapus data,
  • memproses input form,
  • melakukan redirect setelah submit.

Secara konsep, Server Actions tidak menggantikan seluruh kebutuhan API. Jika Anda memerlukan endpoint publik yang diakses aplikasi lain, webhook eksternal, atau kontrak HTTP yang eksplisit, API Route atau Route Handler tetap lebih tepat. Namun untuk mutasi internal yang dipicu dari UI React, Server Actions sering menjadi pilihan yang lebih ergonomis.

Di Next.js App Router, Server Components berjalan di server secara default. Namun tidak semua fungsi otomatis menjadi Server Action. Sebuah fungsi harus ditandai dengan direktif 'use server' agar Next.js memperlakukannya sebagai aksi server yang dapat dipanggil dari form atau mekanisme terkait.

Cara kerja use server dan pemanggilan dari komponen React

Apa itu use server?

'use server' adalah direktif yang memberi tahu Next.js bahwa fungsi atau file tersebut harus dieksekusi di server. Direktif ini penting karena form submission biasanya membawa data dari browser, tetapi validasi dan penyimpanan sebaiknya tetap dilakukan di sisi server agar lebih aman dan konsisten.

Secara praktis, Anda bisa menempatkan Server Action di file terpisah, misalnya app/actions.ts, lalu mengimpornya ke komponen halaman atau komponen form. Pendekatan ini lebih rapi dibanding menaruh semua aksi di satu file halaman.

Struktur project App Router

Berikut contoh struktur project sederhana:

app/
├── actions.ts
├── page.tsx
├── success/
│   └── page.tsx
lib/
└── mock-db.ts

Penjelasan singkat:

  • app/page.tsx: halaman utama yang menampilkan form.
  • app/actions.ts: berisi Server Action untuk memproses form.
  • app/success/page.tsx: halaman tujuan setelah submit berhasil.
  • lib/mock-db.ts: simulasi penyimpanan data sederhana.

Implementasi form submission menggunakan Server Actions

Simulasi penyimpanan data sederhana

Untuk contoh ini, kita gunakan array di memori sebagai simulasi database. Ini hanya untuk pembelajaran. Pada production, gunakan database sungguhan seperti PostgreSQL, MySQL, SQLite, atau service backend yang sesuai.

// lib/mock-db.ts
export type ContactMessage = {
  id: number
  name: string
  email: string
  message: string
  createdAt: Date
}

const messages: ContactMessage[] = []

export async function saveMessage(data: Omit<ContactMessage, 'id' | 'createdAt'>) {
  const newMessage: ContactMessage = {
    id: messages.length + 1,
    ...data,
    createdAt: new Date(),
  }

  messages.push(newMessage)
  return newMessage
}

export async function getMessages() {
  return messages
}

Keterbatasan pendekatan ini:

  • data hilang saat server restart,
  • tidak aman untuk multi-instance deployment,
  • tidak cocok untuk aplikasi nyata.

Namun untuk memahami alur Server Actions, contoh ini cukup jelas.

Validasi data di server

Validasi minimal tetap harus dilakukan di server meskipun form juga memakai validasi HTML di client. Jangan mengandalkan validasi client karena request bisa dimanipulasi.

// app/actions.ts
'use server'

import { redirect } from 'next/navigation'
import { saveMessage } from '@/lib/mock-db'

type FormState = {
  errors?: {
    name?: string
    email?: string
    message?: string
    general?: string
  }
}

function validateInput(formData: FormData): FormState {
  const name = String(formData.get('name') || '').trim()
  const email = String(formData.get('email') || '').trim()
  const message = String(formData.get('message') || '').trim()

  const errors: FormState['errors'] = {}

  if (name.length < 3) {
    errors.name = 'Nama minimal 3 karakter.'
  }

  if (!email.includes('@') || email.length < 5) {
    errors.email = 'Email tidak valid.'
  }

  if (message.length < 10) {
    errors.message = 'Pesan minimal 10 karakter.'
  }

  return Object.keys(errors).length > 0 ? { errors } : {}
}

export async function submitContactForm(
  prevState: FormState,
  formData: FormData
): Promise<FormState> {
  const validation = validateInput(formData)

  if (validation.errors) {
    return validation
  }

  const name = String(formData.get('name') || '').trim()
  const email = String(formData.get('email') || '').trim()
  const message = String(formData.get('message') || '').trim()

  try {
    await saveMessage({ name, email, message })
  } catch {
    return {
      errors: {
        general: 'Gagal menyimpan data. Silakan coba lagi.',
      },
    }
  }

  redirect('/success')
}

Beberapa hal penting pada contoh di atas:

  • 'use server' diletakkan di bagian atas file agar fungsi dieksekusi di server.
  • Validasi dilakukan sebelum penyimpanan data.
  • Jika validasi gagal, action mengembalikan object error.
  • Jika berhasil, action menyimpan data lalu menjalankan redirect('/success').

Mengapa redirect dilakukan di server? Karena setelah mutasi berhasil, server dapat langsung menentukan navigasi berikutnya tanpa menunggu client melakukan router.push(). Ini mengurangi logika imperatif di sisi browser.

Komponen form di app/page.tsx

Berikut contoh halaman yang menggunakan Server Action untuk submit form. Agar state error dari action bisa dipakai di UI, kita gunakan hook React modern untuk action state.

// app/page.tsx
'use client'

import { useActionState } from 'react'
import { submitContactForm } from './actions'

const initialState = {
  errors: {},
}

export default function HomePage() {
  const [state, formAction, isPending] = useActionState(submitContactForm, initialState)

  return (
    <main style={{ maxWidth: 560, margin: '40px auto', fontFamily: 'sans-serif' }}>
      <h1>Form Kontak</h1>
      <p>Contoh submit form dengan Server Actions di Next.js App Router.</p>

      <form action={formAction} style={{ display: 'grid', gap: 12 }}>
        <div>
          <label htmlFor="name">Nama</label>
          <br />
          <input id="name" name="name" type="text" required minLength={3} />
          {state.errors?.name && <p style={{ color: 'crimson' }}>{state.errors.name}</p>}
        </div>

        <div>
          <label htmlFor="email">Email</label>
          <br />
          <input id="email" name="email" type="email" required />
          {state.errors?.email && <p style={{ color: 'crimson' }}>{state.errors.email}</p>}
        </div>

        <div>
          <label htmlFor="message">Pesan</label>
          <br />
          <textarea id="message" name="message" required minLength={10} rows={5} />
          {state.errors?.message && <p style={{ color: 'crimson' }}>{state.errors.message}</p>}
        </div>

        {state.errors?.general && (
          <p style={{ color: 'crimson' }}>{state.errors.general}</p>
        )}

        <button type="submit" disabled={isPending}>
          {isPending ? 'Mengirim...' : 'Kirim'}
        </button>
      </form>
    </main>
  )
}

Di sini ada beberapa detail penting:

  • Komponen diberi 'use client' karena memakai hook interaktif.
  • useActionState membantu menerima state hasil action, termasuk error validasi.
  • Atribut action={formAction} menghubungkan form dengan Server Action.
  • Status isPending berguna untuk mencegah double submit dan memberi umpan balik ke pengguna.

Halaman setelah redirect

// app/success/page.tsx
export default function SuccessPage() {
  return (
    <main style={{ maxWidth: 560, margin: '40px auto', fontFamily: 'sans-serif' }}>
      <h1>Pesan berhasil dikirim</h1>
      <p>Terima kasih, data Anda sudah diproses oleh Server Action.</p>
    </main>
  )
}

Redirect seperti ini cocok untuk pola form klasik: submit berhasil lalu pindah ke halaman sukses. Jika Anda ingin tetap berada di halaman yang sama sambil menampilkan pesan sukses, action dapat mengembalikan state sukses alih-alih melakukan redirect.

Mengapa pendekatan ini bekerja?

Server Actions bekerja karena Next.js mengintegrasikan mekanisme pemanggilan fungsi server dengan pipeline rendering React dan App Router. Ketika form dikirim, Next.js membungkus pemanggilan action sebagai request internal yang aman untuk aplikasi tersebut. Anda tidak perlu membuat URL endpoint sendiri hanya untuk menghubungkan form dengan logika backend.

Keuntungannya:

  • Logika mutasi lebih dekat ke UI: komponen dan aksi yang dipanggilnya berada dalam satu konteks aplikasi.
  • Boilerplate lebih sedikit: tidak perlu menulis handler fetch, parsing JSON, dan response mapping untuk use case sederhana.
  • Validasi tetap di server: data tidak dipercaya dari client.
  • Redirect dan integrasi cache lebih natural: action dapat memanggil utilitas server seperti redirect() atau revalidasi data bila diperlukan.

Namun bukan berarti semua form harus selalu memakai Server Actions. Pilihan terbaik tetap bergantung pada kebutuhan integrasi dan batasan arsitektur.

Server Actions vs API Routes

Kapan pilih Server Actions?

  • Form hanya dipakai oleh UI internal Next.js.
  • Mutasi datanya sederhana dan langsung terkait dengan komponen tertentu.
  • Anda ingin mengurangi boilerplate fetch dan endpoint tambahan.
  • Anda butuh redirect server-side setelah submit.

Kapan tetap pilih API Routes atau Route Handlers?

  • Anda membutuhkan endpoint HTTP publik untuk aplikasi mobile, frontend lain, atau integrasi pihak ketiga.
  • Anda menerima webhook dari service eksternal.
  • Anda butuh kontrak API yang eksplisit, terdokumentasi, dan independen dari React component tree.
  • Anda ingin memisahkan backend interface dari layer UI secara ketat.

Trade-off utama

Server Actions unggul dalam kesederhanaan untuk mutasi internal. Tetapi coupling terhadap aplikasi React/Next.js menjadi lebih kuat. Sementara itu, API Routes memberi fleksibilitas integrasi yang lebih luas, namun dengan lebih banyak lapisan kode.

Aturan praktis: jika pemanggilnya hanya form di aplikasi Next.js Anda sendiri, mulai dari Server Actions. Jika pemanggilnya bisa datang dari luar aplikasi atau perlu antarmuka HTTP yang stabil, gunakan API Route atau Route Handler.

Best practices untuk production

1. Selalu validasi di server

Validasi HTML seperti required atau type="email" hanya membantu UX. Itu bukan mekanisme keamanan. Server Action tetap harus memvalidasi seluruh input.

2. Sanitasi dan normalisasi input

Gunakan trim(), batasi panjang field, dan pertimbangkan sanitasi tambahan bila data akan ditampilkan kembali ke pengguna. Ini membantu mencegah data kotor dan menurunkan risiko bug.

3. Gunakan database nyata untuk data penting

Array di memori hanya cocok untuk demo. Pada production, gunakan penyimpanan persisten dan pastikan operasi tulis memiliki penanganan error yang baik.

4. Tangani status pending dan double submit

Disable tombol submit saat request berjalan, seperti pada contoh isPending. Ini mencegah duplikasi data akibat klik berulang.

5. Jangan menaruh logic sensitif di client component

Server Action membantu menjaga credential, query database, dan logika mutasi tetap di server. Manfaatkan ini dengan benar. Jangan memindahkan operasi sensitif ke browser tanpa alasan.

6. Pertimbangkan autentikasi dan otorisasi

Jika action hanya boleh dipanggil oleh user tertentu, lakukan pemeriksaan session atau token di dalam Server Action sebelum melakukan mutasi data.

7. Tambahkan observability

Untuk production, log error secara terstruktur. Jika submit gagal, simpan context yang cukup untuk debugging, tetapi hindari mencatat data sensitif secara berlebihan.

Kesalahan umum dan tips debugging

Form tidak memanggil action

Pastikan atribut action pada tag <form> benar-benar mengarah ke Server Action atau hasil hook seperti formAction. Jika Anda malah memakai onSubmit dan preventDefault(), alur submit default form tidak akan berjalan.

Lupa menambahkan 'use server'

Jika fungsi action tidak ditandai dengan benar, Next.js tidak akan memperlakukannya sebagai Server Action. Ini adalah salah satu penyebab error yang paling sering terjadi.

Data tidak tersimpan

Periksa apakah validasi gagal secara diam-diam, apakah field name di form cocok dengan formData.get(...), dan apakah lapisan penyimpanan melempar exception. Tambahkan logging sementara di server untuk memverifikasi alur.

Redirect tidak terjadi

Pastikan kode tidak mengembalikan state error sebelum mencapai redirect(). Ingat bahwa redirect() menghentikan alur normal eksekusi karena melakukan navigasi server-side.

Penutup

Server Actions di Next.js 16 menawarkan pendekatan yang lebih langsung untuk menangani form submission pada App Router tanpa harus membuat API Route terpisah untuk setiap mutasi sederhana. Dengan pola ini, form dapat memanggil fungsi server secara langsung, validasi tetap dilakukan di backend, data dapat disimpan dengan aman, dan redirect setelah submit menjadi lebih mudah dikelola.

Meski demikian, Server Actions bukan pengganti mutlak API Routes. Gunakan Server Actions untuk mutasi internal yang erat dengan UI React, dan gunakan API Routes ketika Anda memerlukan endpoint HTTP yang lebih umum, publik, atau dipakai lintas aplikasi.

Jika Anda sedang membangun form internal seperti contact form, profile update, atau create-post flow di App Router, Server Actions sering menjadi pilihan yang lebih sederhana, lebih konsisten, dan lebih nyaman dirawat dibanding pola API endpoint terpisah.