π§ 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:
- Tangkap datanya di client (browser),
- Kirim ke API Route pakai
fetch, - Terima respons dari API,
- 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. Misalnyaapp/api/submit/route.ts, terus baru bisa kamu panggil viafetch().
β 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

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:

π§± 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.

Untuk password database bisa ke halaman Project Settings β Database, klik 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

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

π§ͺ Contoh Sederhana: Bikin Form Tambah Post
Biar makin kebayang cara kerja Server Actions, kita langsung aja bikin form sederhana buat tambah post. Kita akan:
- Bikin Server Action buat simpan data post ke database
- Panggil action-nya dari komponen form
- 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 serverdi 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
- 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.
- Form (
PostForm):- Pakai
useFormAction()buat handle respon dari server. formActiondikaitkan langsung ke form<form action={formAction}>.- Saat user submit,
Server Actionotomatis dijalankan. - Bisa munculin pesan feedback via
state.errors.title,state.errors.content.
- Pakai
Jalan server dengan perintah ini:
bun dev
Maka akan tampil serperti ini:

Tambahkan post

Maka daftar post akan otomatis bertambah

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
onClickbiasa 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
useStatedi 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.