Membangun aplikasi fullstack pertama dengan Next.js App Router: memahami Server Components, Server Actions, dan alur data fullstack — dari formulir di browser sampai baris di database — dalam satu codebase.

Setelah di episode 2 kita memilih arsitektur (Next.js server-rendered + database), pada episode ini kita menulis kode sungguhan. Ini episode pertama yang benar-benar hands-on — kalian akan membangun aplikasi fullstack kecil bernama catatan (notes) dan merasakan sendiri bagaimana meta-framework meniadakan batas frontend-backend.
Mengapa Next.js? Karena ia adalah meta-framework fullstack paling matang di 2026: App Router sebagai model routing, Server Components sebagai cara rendering, dan Server Actions sebagai jembatan data tanpa menulis REST endpoint. Memahami tiga konsep ini berarti memahami inti dari "fullstack modern".
Gunakan create-next-app — ia menyiapkan struktur, TypeScript, Tailwind, dan ESLint sekaligus:
pnpm create next-app@latest catatan-fullstack \
--typescript --tailwind --eslint --app \
--src-dir --import-alias "@/*"
cd catatan-fullstack
pnpm devBuka http://localhost:3000 — halaman default Next.js akan tampil. Sekarang mari bedah struktur yang terbentuk.
Pada App Router, setiap folder di dalam src/app adalah sebuah route. Konvensinya sederhana:
| Folder/File | Fungsi |
|---|---|
src/app/page.tsx | Route / |
src/app/notes/page.tsx | Route /notes |
src/app/notes/[id]/page.tsx | Route dinamis /notes/1, /notes/2 |
src/app/layout.tsx | Layout yang membungkus semua halaman |
File layout.tsx penting: ia merender sekali dan dipertahankan saat navigasi antar halaman — tidak seperti model page-per-page klasik:
import type { Metadata } from "next"
export const metadata: Metadata = {
title: "Catatan",
description: "Aplikasi catatan fullstack",
}
export default function RootLayout({
children,
}: Readonly<{ children: React.ReactNode }>) {
return (
<html lang="id">
<body className="max-w-2xl mx-auto p-6">
<header>
<h1 className="text-2xl font-bold">Catatan</h1>
</header>
<main>{children}</main>
</body>
</html>
)
}Konsep kunci Next.js: semua komponen adalah Server Component secara default. Artinya komponen dieksekusi di server — bisa mengakses database, file, dan secrets — lalu hasil HTML-nya dikirim ke browser. JavaScript yang dikirim ke client hanya untuk interaktivitas.
import { getNotes } from "@/lib/db"
export default async function NotesPage() {
const notes = await getNotes()
return (
<ul>
{notes.map((note) => (
<li key={note.id}>
<h2>{note.title}</h2>
<p>{note.body}</p>
</li>
))}
</ul>
)
}Perhatikan: fungsi async langsung dipanggil di dalam komponen, dan getNotes() membaca dari database di server. Tidak ada fetch, tidak ada loading spinner, tidak ada API. Ini adalah fullstack data flow paling ringkas yang pernah ada.
Tambahkan "use client" di baris pertama file hanya saat butuh interaktivitas browser: useState, useEffect, event handler, atau API browser. Aturan praktis: makin sedikit client component, makin cepat aplikasi.
"use client"
import { useState } from "react"
export default function Counter() {
const [count, setCount] = useState(0)
return <button onClick={() => setCount(count + 1)}>{count}</button>
}Bagian paling menakjubkan: menulis data ke database tanpa membuat endpoint API. Fungsi "use server" dipanggil langsung dari form HTML. Next.js mengirim request ke server action secara otomatis, menjalankannya, dan merevalidasi tampilan:
import { db } from "@/lib/db"
import { revalidatePath } from "next/cache"
import { redirect } from "next/navigation"
async function createNote(formData: FormData) {
"use server"
const title = String(formData.get("title") ?? "")
const body = String(formData.get("body") ?? "")
const note = await db.note.create({
data: { title, body },
})
revalidatePath("/notes")
redirect(`/notes/${note.id}`)
}
export default function NewNote() {
return (
<form action={createNote}>
<input name="title" required placeholder="Judul" />
<textarea name="body" required placeholder="Isi" />
<button type="submit">Simpan</button>
</form>
)
}Alurnya lengkap end-to-end: form di browser → createNote di server → db.note.create → redirect + revalidasi. Satu file, dua sisi, tanpa API layer.
Warning
Server Actions tetap butuh validasi dan proteksi CSRF — keamanan tidak otomatis hilang. Kita bahas validasi di episode 7 dan keamanan CSRF/XSS di episode 16. Untuk sekarang, cukup pahami alurnya.
Inilah lingkaran fullstack yang kita bahas di episode 1 — sekarang kalian melihatnya hidup dalam kode nyata.
Selesaikan praktik dengan tiga langkah:
/notes yang menampilkan daftar catatan (seperti kode di atas)./notes/new dengan form yang memanggil server action createNote.getNotes dan db sementara memakai data dummy array (database asli menyusul di episode 4).export type Note = { id: string; title: string; body: string }
const notes: Note[] = [
{ id: "1", title: "Pertama", body: "Belajar Server Components" },
{ id: "2", title: "Kedua", body: "Belajar Server Actions" },
]
export async function getNotes(): Promise<Note[]> {
return notes
}Jalankan pnpm dev dan coba tambahkan catatan dari form. Jika berhasil, kalian baru saja membangun fitur fullstack pertama — tanpa menulis satu pun endpoint API.
Inti yang harus dibawa pulang:
layout.tsx mempertahankan UI antar navigasi.Di episode 4 selanjutnya kita akan mengganti data dummy dengan database & ORM sungguhan — Prisma/Drizzle, migration, dan query optimization — plus praktik model + CRUD yang akan dipakai semua episode berikutnya. Pastikan PostgreSQL dari episode 0 sudah berjalan, karena kita akan menyambungkannya sekarang!