Apa Itu Server Actions di Next.js 15? Panduan Lengkap untuk Pemula

🧠 Pembuka: β€œEh, Emang Apa Sih Server Actions Itu?”

Kalau kamu udah pernah ngoding pakai Next.js sebelum versi 13, pasti familiar banget sama yang namanya API Route. Ya, semacam "pintu belakang" buat ngambil atau ngirim data. Misal, mau simpan data form ke database? Bikin pages/api/form.ts. Mau update data? Lagi-lagi lewat API Route. Ribet? Nggak juga… tapi kadang terasa muter-muter.

Nah, di Next.js 15, muncullah Server Actions β€” fitur baru yang bikin interaksi sama server jadi jauh lebih langsung, bersih, dan efisien. Ibaratnya, kamu nggak perlu lagi bikin dapur sendiri cuma buat goreng telur. Cukup masak langsung di meja makan. πŸ˜„

Server Actions ini cocok banget buat kamu yang pengen:

  • Kirim data dari form tanpa ribet setup API.
  • Jalankan logic di server tanpa bolak-balik fetch.
  • Punya komponen yang logikanya nempel sama UI-nya.

Jadi di artikel ini, kita bakal bahas lengkap:

  • βœ… Apa itu Server Actions?
  • βš™οΈ Gimana cara kerjanya?
  • βš–οΈ Apa kelebihan dan kekurangannya?
  • πŸ“Œ Dan, kapan sih kamu sebaiknya pakai fitur ini?

Siapin kopi atau teh dulu… karena kita bakal kupas tuntas Server Actions dengan gaya santai, tanpa bikin kening berkerut. πŸ˜‰


πŸ“¦ Apa Itu Server Actions?

Oke, sekarang kita masuk ke inti dari semuanya: apa itu Server Actions?

Bayangin gini…

Biasanya, kalau kamu mau ngirim data ke server (misalnya, dari form input user), kamu harus:

  1. Tangkap datanya di client (browser),
  2. Kirim ke API Route pakai fetch,
  3. Terima respons dari API,
  4. Baru deh update tampilan atau kasih feedback ke user.

Rasanya kayak kamu mau ngirim surat, tapi harus:

  • Ke kantor pos dulu,
  • Ambil formulir,
  • Masukin ke kotak surat,
  • Tunggu kurir nganterin ke tujuan…

Lama dan ribet, kan?

Nah, Server Actions ini bikin semuanya jadi lebih simpel.

Analogi sederhananya: kayak kamu langsung kasih surat ke penerima, tanpa lewat banyak tangan. πŸ”„πŸ“©

Jadi, secara teknis:

Server Actions adalah fungsi async yang berjalan di server, tapi kamu bisa panggil langsung dari komponen React, khususnya form.

Gak perlu fetch(), gak perlu bikin file di /api, gak perlu muter-muter.

Cocok banget buat:

  • βœ… Form submission β€” contoh: kirim data kontak, register user, tambah task.
  • πŸ” Mutasi data β€” kayak update profil, hapus item, atau ubah status.
  • βš™οΈ Interaksi langsung dengan database β€” tanpa perlu tulis kode fetch manual ke API Route.

Intinya, Server Actions itu solusi baru buat interaksi server yang lebih langsung, rapi, dan terintegrasi sama komponen.


πŸ” Bedanya Server Actions vs API Route

Mungkin kamu mulai mikir,

β€œLah, bukannya fungsi kirim data ke server udah bisa pakai API Route? Emang bedanya apa sama Server Actions?”

Pertanyaan bagus! Kita ibaratkan kayak dua jalur buat nganter paket:

  • πŸ“¦ API Route itu jalur lama, kamu harus nganter paket ke kantor logistik dulu.
  • βœ‰οΈ Server Actions itu jalur baru, kamu langsung titipin ke orang dalam. Lebih cepat, lebih dekat.

Biar lebih jelas, yuk lihat perbandingan di bawah ini:

βœ… 1. Lokasi Kode

  • Server Actions: Bisa lansung kamu tulis di dalam file komponen atau folder khusus kayak actions/submit-task.ts. Praktis, gak perlu bikin folder /api.
  • API Route: Harus bikin file sendiri di folder /api. Misalnya app/api/submit/route.ts, terus baru bisa kamu panggil via fetch().

βœ… 2. Response

  • Server Actions: Bisa langsung dipakai di UI. Misalnya kamu submit form, bisa langsung balikin feedback atau error tanpa muter-muter.
  • API Route: Harus dipanggil pakai fetch, axios, atau sejenisnya. Hasilnya baru bisa diproses di client-side.

βœ… 3. Use Case (Kapan Cocok Dipakai?)

  • Server Actions: Ideal buat yang ringan-ringan dan nempel sama UI, kayak:
    • Submit form
    • Tambah/update data
    • Hapus item
  • API Route: Lebih cocok untuk:
    • Operasi yang berat dan kompleks
    • Akses ke API eksternal (misalnya fetch ke layanan pihak ketiga)
    • Endpoint yang juga dipakai di mobile app atau layanan lain

βœ… 4. Keamanan

  • Server Actions: Otomatis dijalankan di server β€” jadi gak bisa dilihat/dijalankan langsung dari browser. Udah aman secara default.
  • API Route: Harus kamu amankan sendiri. Harus cek auth, validasi input, dan pastikan gak bisa diakses sembarangan.

🧩 Singkatnya:

  • Kalau kamu butuh fungsi simpel yang dekat dengan UI ➜ Server Actions jawabannya.
  • Kalau kamu butuh API umum atau logic yang kompleks ➜ API Route tetap lebih cocok.

Keduanya gak saling gantiin, tapi saling melengkapi. Tinggal disesuaikan aja sama kebutuhan project kamu.

πŸ› οΈ Persiapan Proyek: Install Next.js + Drizzle + Supabase

Sebelum kita masuk ke bagian coding Server Actions, ada baiknya kita siapin dulu proyeknya dari nol. Tenang, gampang kok β€” kamu cuma butuh Bun, Next.js 15, dan beberapa tool pendukung.


🧩 1. Install Bun (Kalau Belum Ada)

Kalau kamu belum pakai Bun, install dulu via terminal:

powershell -c "irm bun.sh/install.ps1|iex"

Setelah itu, restart terminal kamu dan pastikan Bun sudah terpasang:

bun -v
Versi Bun
Versi Bun

Tentu kalian juga bisa menugunakan npm , pnpm, atau yarn tinggal sesuaikan command nya.


πŸš€ 2. Inisialisasi Proyek Next.js 15

Sekarang kita buat project baru pakai create-next-app versi App Router (Next.js 15+):

bunx create-next-app@latest bwa-server-action

Saat prompt muncul, pastikan kamu pilih:

Install Next.js
Install Next.js

🧱 3. Install Drizzle ORM + Supabase

Sekarang kita tambahkan tools backend-nya. Kita akan pakai:

  • Drizzle ORM buat komunikasi ke database
  • Supabase sebagai database dan layanan auth kita
bun add drizzle-orm drizzle-zod zod postgres; bun add -D drizzle-kit

Sekarang buat file .env pada root proyek dan tambahkan kode ini:

# Connect to Supabase via connection pooling with Supavisor.
DATABASE_URL="postgres://postgres.[PROJECT_ID]:[DB_PASSWORD]@aws-0-ap-southeast-1.pooler.supabase.com:6543/postgres?pgbouncer=true"

# Direct connection to the database. Used for migrations.
DIRECT_URL="postgres://postgres.[PROJECT_ID]:[DB_PASSWORD]@aws-0-ap-southeast-1.pooler.supabase.com:5432/postgres"

DB_SCHEMA_NAME="bwa_dev"

Sesuaikan PROJECT_ID dan DB_PASSWORD dengan proyek di supabase kalian.

Kalian bisa buat proyek baru di supabase atau gunakan yang sudah ada, untuk melihat proyek id masuk ke Project Settings β†’ General.

Supabase Dashboard
Supabase Dashboard

Untuk password database bisa ke halaman Project Settings β†’ Database, klik Reset database password

Reset Database Password
Reset Database Password

Selanjutnya buat file drizzle.config.ts pada root proyek dan tambahkan kode ini:

// drizzle.config.ts
import { defineConfig } from "drizzle-kit";

export default defineConfig({
  schema: "./src/db/schema/index.ts",
  out: "./drizzle",
  dialect: "postgresql",
  schemaFilter: ["public", process.env.DB_SCHEMA_NAME!],
  dbCredentials: {
    url: process.env.DIRECT_URL!,
  },
  // Print all statements
  verbose: true,
  // Always ask for confirmation
  strict: true,
});

Sekarang kita akan buat konfigurasi schema database, buat file index.ts pada src/db/index.ts dan salin kode ini:

// src/db/index.ts
import * as schema from "./schema";
import postgres from "postgres";
import { drizzle } from "drizzle-orm/postgres-js";

const client = postgres(
  process.env.NODE_ENV === "production"
    ? process.env.DATABASE_URL! // Use DATABASE_URL in production for pooling
    : process.env.DIRECT_URL!, // Use DIRECT_URL in development for direct access
);
export const db = drizzle(client, { schema });

Selanjutnya buat schema database, buat file schema.ts pada src/db/schema/schema.ts dan salin kode ini:

// src/db/schema/schema.ts
import { pgSchema } from "drizzle-orm/pg-core";

export const dbSchema = pgSchema(process.env.DB_SCHEMA_NAME!);

Sekarang buat file index.ts pada src/db/schema/index.ts dan export schema yang tadi kita buat. File ini akan mengekspor schema yang kita buat.

// src/db/schema/index.ts
export * from "./schema";
export * from "./post";

Kemudian buat file task.ts pada src/db/schema/task.ts dan salin kode ini:

// src/db/schema/post.ts
import { text, timestamp, uuid } from "drizzle-orm/pg-core";
import { dbSchema } from ".";
import { createInsertSchema, createUpdateSchema } from "drizzle-zod";

export const post = dbSchema.table("post", {
  id: uuid("id").primaryKey().defaultRandom(),
  title: text("title").notNull(),
  content: text("content"),
  createdAt: timestamp("created_at").defaultNow(),
});

export const PostInsertSchema = createInsertSchema(post);

export const PostUpdateSchema = createUpdateSchema(post);

Jika semua sudah selesai, sekarang buka file package.json lalu tambahkan kode ini pada bagian scripts :

"scripts": {
    ...
    "db:push": "drizzle-kit push",
    "db:studio": "drizzle-kit studio --port 5555 --verbose"
  },

Selanjutnya kita push schema ke database dengan perintah ini:

bun db:push
Push Database
Push Database

Kita bisa cek di dashboard supabase atau kita bisa gunakan drizzle studio

bun db:studio

Drizzle studio bisa diakses pada link ini https://local.drizzle.studio/?port=5555

Drizzle Studio
Drizzle Studio

πŸ§ͺ Contoh Sederhana: Bikin Form Tambah Post

Biar makin kebayang cara kerja Server Actions, kita langsung aja bikin form sederhana buat tambah post. Kita akan:

  1. Bikin Server Action buat simpan data post ke database
  2. Panggil action-nya dari komponen form
  3. Tampilkan hasilnya di UI

πŸ“ Step 1: Bikin Server Action

Buat file untuk create post di src/actions/post/create.ts, lalu tambahkan kode ini:

// src/actions/post/create.ts
"use server";

import { db } from "@/db";
import { post, PostInsertSchema, PostInsertValues } from "@/db/schema";
import { revalidatePath } from "next/cache";
import * as z from "zod/v4";

export interface ActionResponse {
  success: boolean;
  message: string;
  errors?: {
    [K in keyof PostInsertValues]?: string[];
  };
}

export async function createPost(
  prevState: ActionResponse | undefined,
  formData: FormData
): Promise<ActionResponse> {
  const postData = Object.fromEntries(formData);
  console.log("post:", postData);
  const validatePost = PostInsertSchema.safeParse(postData);
  console.log("validate:", validatePost);

  if (!validatePost.success) {
    const errors = z.flattenError(validatePost.error);

    return {
      success: false,
      message: "Please fix the errors in the form",
      errors: errors.fieldErrors,
    };
  }

  await db.insert(post).values(validatePost.data);

  revalidatePath("/");
  return {
    success: true,
    message: "Post saved successfully!",
  };
}

πŸ”’ use server di atas penting banget β€” itu penanda bahwa fungsi ini bakal jalan di server, bukan client.

Kemudian buat file untuk ambil daftar post di src/actions/post/queries.ts dan tambahkan kode ini:

// src/actions/post/queries.ts
"use server";

import { db } from "@/db";
import { post } from "@/db/schema";

export async function getAllPosts() {
  return await db.select().from(post).orderBy(post.createdAt);
}

Setelah itu buat file untuk delete post di src/actions/post/delete.ts, lalu tambahkan kokde ini:

// src/actions/post/delete.ts
"use server";

import { db } from "@/db";
import { post } from "@/db/schema";
import { eq } from "drizzle-orm";
import { revalidatePath } from "next/cache";

export async function deletePost(id: string) {
  const [postData] = await db.select().from(post).where(eq(post.id, id));

  if (!postData) {
    return {
      message: "Post not found",
    };
  }

  await db.delete(post).where(eq(post.id, id));

  revalidatePath("/");
  return {
    message: "Post deleted",
  };
}

πŸ“„ Step 2: Bikin Form Input

Buat file post-form.tsx pada src/components/post-form.tsx dan tambahkan kode ini:

// src/components/post-form.tsx
"use client";

import { useActionState } from "react";
import { ActionResponse, createPost } from "@/actions/post/create";

const initialState: ActionResponse = {
  success: false,
  message: "",
};

export function PostForm() {
  const [state, formAction, isPending] = useActionState(
    createPost,
    initialState
  );

  return (
    <form action={formAction} className="space-y-4 max-w-md">
      <div>
        <label htmlFor="title" className="block font-medium">
          Judul Post
        </label>
        <input
          type="text"
          name="title"
          id="title"
          required
          className="w-full border rounded p-2"
        />
        {state?.errors?.title && (
          <p id="title-error" className="text-sm text-red-500">
            {state.errors.title[0]}
          </p>
        )}
      </div>

      <div>
        <label htmlFor="content" className="block font-medium">
          Konten
        </label>
        <textarea
          name="content"
          id="content"
          required
          className="w-full border rounded p-2"
        />
        {state?.errors?.content && (
          <p id="content-error" className="text-sm text-red-500">
            {state.errors.content[0]}
          </p>
        )}
      </div>

      <button
        type="submit"
        className="w-full bg-blue-500 text-white py-2 rounded-md"
        disabled={isPending}
      >
        {isPending ? "Saving..." : "Save Address"}
      </button>
    </form>
  );
}

Kemudian buat file post-item.tsx pada src/components/post-item.tsx dan tambahkan kode ini:

// src/components/post-item.tsx
"use client";

import { deletePost } from "@/actions/post/delete";
import { PostInsertValues } from "@/db/schema";
import React, { useTransition } from "react";

const PostItem = ({ id, title, content, createdAt }: PostInsertValues) => {
  const formattedDate = createdAt
    ? new Date(createdAt).toLocaleString("id-ID", {
        dateStyle: "long",
        timeStyle: "short",
      })
    : "Tanggal tidak tersedia";

  const [isPending, startTransition] = useTransition();

  const onDelete = () => {
    startTransition(async () => {
      if (!id) {
        alert("Post Id not found");
      }
      const response = await deletePost(id!);
      alert(response.message);
    });
  };

  return (
    <div className="flex flex-col rounded space-y-2">
      <h3 className="font-semibold text-lg">{title}</h3>
      <p className="text-gray-700">{content}</p>
      <time className="block text-sm text-gray-500">{formattedDate}</time>
      <button
        onClick={onDelete}
        className="ml-auto bg-red-500 text-white px-3 py-1 rounded text-sm disabled:opacity-50"
        disabled={isPending}
      >
        {isPending ? "Menghapus..." : "Hapus"}
      </button>
    </div>
  );
};

export default PostItem;

πŸ“ƒ 3. Tambahkan Form ke Halaman

Sekarang buka file src/app/page.tsx dan ubah jadi seeperti berikut ini:

// src/app/page.tsx
import { getAllPosts } from "@/actions/post/queries";
import { PostForm } from "@/components/post-form";
import PostItem from "@/components/post-item";

export default async function Home() {
  const posts = await getAllPosts();
  return (
    <main className="max-w-xl mx-auto px-4 py-10 space-y-8">
      <h1 className="text-3xl font-bold">πŸ“ Tambah Post Baru</h1>

      <PostForm />

      <div className="space-y-4">
        <h2 className="text-2xl font-semibold mt-10">πŸ“ƒ Daftar Post</h2>

        {posts.length === 0 ? (
          <p className="text-gray-500">Belum ada post.</p>
        ) : (
          <ul className="space-y-3">
            {posts.map((post) => (
              <li key={post.id} className="border rounded p-4">
                <PostItem {...post} />
              </li>
            ))}
          </ul>
        )}
      </div>
    </main>
  );
}

🧠 Step-by-Step Penjelasan

  1. Server Action (createPost, deletePost, getAllPosts):
    • Dijalankan di server.
    • Ambil data dari FormData.
    • Simpan ke database lewat db.insert(...).
    • revalidatePath("/") digunakan agar data terbaru langsung tampil di homepage.
  2. Form (PostForm):
    • Pakai useFormAction() buat handle respon dari server.
    • formAction dikaitkan langsung ke form <form action={formAction}>.
    • Saat user submit, Server Action otomatis dijalankan.
    • Bisa munculin pesan feedback via state.errors.title, state.errors.content.

Jalan server dengan perintah ini:

bun dev

Maka akan tampil serperti ini:

Homepage
Homepage

Tambahkan post

Tambahkan Post
Tambahkan Post

Maka daftar post akan otomatis bertambah

Daftar Post
Daftar Post

Untuk hapus post caranya klik tombol Hapus


πŸš€ Kelebihan Server Actions (Kenapa Perlu Coba?)

Kita semua suka sesuatu yang praktis, aman, dan efisien, kan? Nah, itu sebabnya Server Actions di Next.js 15 mulai banyak dilirik developer.

Yuk kita lihat kenapa fitur ini layak banget dicoba:


πŸ”’ Lebih Aman

Karena Server Actions dijalankan langsung di server, logic-nya nggak bisa diintip atau dimodifikasi dari browser.

Misalnya kamu punya form untuk update profil β€” user nggak bisa tiba-tiba nge-inject role: "admin" dari DevTools. Semuanya aman di balik layar.

Jadi nggak perlu khawatir "form saya bisa dimainin gak ya?" β€” jawabannya: nggak bisa.


🧹 Lebih Bersih

Biasanya, kalau mau submit form, kita harus bikin file /api/post, terus fetch() dari client, lalu handle response lagi.

Ribet? Iya.

Dengan Server Actions, kamu cukup nulis function di file actions.ts, lalu panggil langsung dari form. Simpel dan gak bikin folder api/ kamu penuh debu.


⚑ Lebih Cepat

Karena gak perlu fetch(), useEffect(), atau library tambahan kayak Axios, responsenya bisa langsung dipakai di komponen.

Feedback ke user juga lebih instan, misal langsung muncul error validasi atau pesan sukses.

Gak ada lagi loading state yang ribet ngatur manual. Tinggal pakai useFormState atau useActionState.


🎯 Closer to Component

Biasanya logic dan UI jauh-jauhan: logic di /api, tampilan di /components.

Dengan Server Actions, semuanya bisa lebih deket. Bahkan kamu bisa punya logic submit langsung di dalam file komponen!

Ini bikin kamu lebih fokus ngebangun fitur, bukan ngatur folder.


⚠️ Kekurangan & Hal yang Perlu Diwaspadai

Server Actions memang keren β€” tapi bukan berarti tanpa celah. Ada beberapa hal yang perlu kamu tahu sebelum all-in:


🐌 Belum Didukung Semua Hosting

Kalau kamu hosting di Vercel, aman, tinggal gas!

Tapi kalau pakai platform lain kayak Netlify atau self-hosting, fitur ini mungkin belum bisa jalan penuh β€” terutama karena Server Actions butuh environment yang dukung RSC (React Server Components) dan routing Edge/Server Function modern.

Jadi sebelum pakai, pastikan tempat hosting kamu udah siap nerima fitur ini. Jangan sampai kamu happy ngoding, tapi pas deploy malah gagal total. πŸ˜…


πŸ“¦ Harus Pakai Form

Ini penting: Server Actions saat ini hanya bisa dipicu lewat <form>.

Artinya?

  • Gak cocok buat tombol-tombol dinamis yang butuh interaksi cepat (kayak voting, drag-and-drop, atau real-time UI).
  • Gak bisa dipanggil langsung dari onClick biasa tanpa form submit.

Jadi kalau use case kamu super interaktif atau heavily dynamic, mungkin masih lebih cocok pakai API route atau fetch manual.


🀯 Server-Client Boundary Bisa Bikin Bingung

Ini yang kadang nyangkut di kepala:

β€œLah ini function di server atau client sih?”

β€œKenapa gak bisa pakai useState di sini?”

β€œKok console.log() gak muncul di browser?”

Karena Server Actions berjalan di server, kamu harus mulai paham soal "boundary" antara client & server. Salah taruh kode, bisa bikin error yang membingungkan.

Tapi tenang β€” begitu kamu paham alurnya, semua akan terasa masuk akal. 😎


πŸ“Œ Kapan Sebaiknya Kamu Pakai Server Actions?

Server Actions itu ibarat alat baru di toolbox kamu. Tapi bukan berarti semua masalah harus diselesaikan pakai palu ini.

Biar gak salah tempat, yuk kita bahas: kapan cocok pakai, dan kapan sebaiknya jangan dulu.


βœ… Gunakan Server Actions Kalau...

πŸ“¨ 1. Form Submission Sederhana

Contohnya: form tambah post, komentar, kontak, dll.

Gak butuh interaksi real-time? Server Actions bisa jadi pilihan terbaik: aman, langsung, dan gak ribet.

πŸ› οΈ 2. CRUD Langsung dari Komponen

Kamu bisa bikin tombol tambah, edit, delete langsung di UI tanpa harus nyiapin folder api/.

Semua logika bisa bareng dengan tampilan.

πŸ†• 3. Proyek Baru dengan Next.js 15

Kalau kamu mulai dari nol, kenapa gak langsung manfaatin fitur terbaru?

Server Actions akan terasa seamless kalau kamu udah full pake App Router dan Server Components.


🚫 Hindari Server Actions Kalau...

🌐 1. Kamu Mau Akses API Eksternal

Misalnya kamu butuh ambil data dari REST API lain atau third-party API kayak Stripe, Supabase, dsb β€” lebih aman dan fleksibel pakai API Route biasa atau fetch() dari server.

πŸ“± 2. Perlu REST API untuk Mobile App

Kalau proyek kamu punya mobile client (misal pakai Flutter atau React Native), kamu tetap perlu endpoint tradisional β€” dan Server Actions bukan solusi di sini.

πŸ—οΈ 3. Masih Pakai Pages Router

Fitur ini cuma tersedia di App Router (alias folder app/, bukan pages/).

Kalau proyekmu masih pakai Pages Router lama, ya... Server Actions gak akan bisa dipakai.


Jadi intinya: pakai Server Actions untuk hal-hal yang dekat sama UI dan gak perlu expose ke luar.

Kalau butuh fleksibilitas lebih luas atau komunikasi dengan dunia luar, kamu masih perlu api/ route.

🎯 Kesimpulan: Server Actions = Simpel Tapi Powerful

Server Actions di Next.js 15 itu kayak alat baru yang praktis banget buat ngebangun web app.

Dengan fitur ini, kamu bisa:

  • πŸ’¨ Bikin form yang langsung nyambung ke logic backend
  • 🧹 Nulis kode lebih bersih tanpa folder api/ berantakan
  • πŸ” Dapatkan keamanan ekstra karena logic cuma bisa jalan di server

Tapi ingat ya...

Server Actions bukan obat mujarab untuk semua kasus.

Kalau kamu asal ganti semua API Route jadi Server Actions tanpa pertimbangan, bisa-bisa malah ribet sendiri β€” apalagi kalau ada kebutuhan eksternal, mobile client, atau sistem interaktif non-form.


πŸ”„ Jadi, kapan sebaiknya pakai?

  • Kalau kamu mau bikin form sederhana
  • Mau logic deket sama UI
  • Dan jalanin semuanya di Next.js 15 dengan App Router

...maka Server Actions adalah sahabat baru yang siap bantuin kamu.

Tapi kalau butuh fleksibilitas ekstra, REST API, atau integrasi luas?

API Route masih tetap relevan.


Kesimpulannya: Server Actions itu alternatif, bukan pengganti.

Gunakan dengan bijak, dan kamu bakal dapet pengalaman ngoding yang lebih clean, efisien, dan aman.