Memahami kapan memakai REST API dan kapan server actions, membangun route handlers di Next.js, pola error handling yang konsisten, serta integrasi API eksternal dari sisi server dengan timeout dan retry.

Pada episode 4 kita menulis data langsung dari server actions. Tapi fullstack dunia nyata tidak selalu semudah itu: ada saatnya kalian harus membangun API publik, menyambungkan layanan eksternal, dan menangani error dari sistem yang tidak kalian kontrol. Episode ini membahas API design & integration: kapan REST, kapan GraphQL, kapan cukup server actions, dan bagaimana pola error handling yang tidak akan membuat kalian bangun tengah malam karena bug.
Server Actions (episode 3-4) menangani form internal dengan sangat baik. Tapi ia bukan jawaban untuk semua hal:
| Kebutuhan | Pilih |
|---|---|
| Form internal aplikasi | Server Actions |
| API publik untuk third-party | REST route handler |
| Konsumen mobile/native | REST (bukan server action) |
| Client dengan kebutuhan field fleksibel | GraphQL |
| Webhook yang dipanggil sistem lain | REST |
Aturan praktis: jika konsumennya adalah browser + aplikasi kalian sendiri, server actions cukup. Jika konsumennya sistem lain (mobile, partner, webhook), kalian butuh API eksplisit. Ini alasan mengapa kita memakai server-rendered (episode 2) sekaligus tetap menguasai REST.
App Router memungkinkan route handler dengan ekspor fungsi HTTP di file route.ts:
import { NextResponse } from "next/server"
import { prisma } from "@/lib/prisma"
export async function GET() {
const notes = await prisma.note.findMany({
where: { published: true },
select: { id: true, title: true },
})
return NextResponse.json({ data: notes })
}
export async function POST(request: Request) {
const body = await request.json()
const note = await prisma.note.create({
data: {
title: body.title,
body: body.body,
authorId: "user-1",
},
})
return NextResponse.json({ data: note }, { status: 201 })
}URL-nya adalah GET /api/notes dan POST /api/notes. Satu file untuk satu resource, dengan setiap method HTTP sebagai fungsi — pola yang konsisten dan mudah dipelajari.
Ini bagian yang paling sering diremehkan, padahal API tanpa error handling yang konsisten adalah mimpi buruk integrasi. Tiga prinsip:
import { NextResponse } from "next/server"
import { prisma } from "@/lib/prisma"
export async function GET(
_request: Request,
{ params }: { params: Promise<{ id: string }> },
) {
const { id } = await params
const note = await prisma.note.findUnique({ where: { id } })
if (!note) {
return NextResponse.json(
{ error: "Catatan tidak ditemukan" },
{ status: 404 },
)
}
return NextResponse.json({ data: note })
}Kontrak error yang konsisten: { error: string } untuk semua kegagalan, dengan status code yang mencerminkan masalahnya — 400 input salah, 401 belum login, 403 tidak berhak, 404 tidak ditemukan, 500 error server.
Jangan pernah percaya data dari client. Validasi payload dengan schema (misalnya Zod) sebelum menyentuh database:
import { z } from "zod"
const createNoteSchema = z.object({
title: z.string().min(1).max(120),
body: z.string().min(1).max(10000),
})
export async function POST(request: Request) {
const body = await request.json().catch(() => null)
const parsed = createNoteSchema.safeParse(body)
if (!parsed.success) {
return NextResponse.json(
{ error: "Input tidak valid", issues: parsed.error.issues },
{ status: 400 },
)
}
const note = await prisma.note.create({ data: parsed.data })
return NextResponse.json({ data: note }, { status: 201 })
}Zod menutup celah type safety: data yang masuk sudah terjamin bentuknya sebelum dipakai.
Kesalahan umum: mengembalikan stack trace atau pesan error database mentah ke client. Log detail di server, kirim pesan generik ke client:
try {
// operasi database
} catch (error) {
console.error("[api] create note failed", error)
return NextResponse.json(
{ error: "Terjadi kesalahan pada server" },
{ status: 500 },
)
}Warning
Pesan error mentah dari database (misalnya constraint violation PostgreSQL) sering mengandung nama tabel, kolom, dan konteks internal yang bisa dimanfaatkan penyerang. Log penuh di server, ringkas di response.
Fullstack juga berarti berintegrasi dengan sistem pihak ketiga — pembayaran, email, AI. Aturan penting: lakukan dari sisi server, bukan browser. Secrets tidak boleh sampai ke client, dan CORS tidak perlu repot diurus.
Pola yang aman: server component memanggil API eksternal dengan timeout dan retry:
export async function createPayment(amount: number) {
const controller = new AbortController()
const timeout = setTimeout(() => controller.abort(), 10_000)
try {
const res = await fetch("https://api.payment.example/v1/charge", {
method: "POST",
headers: {
"Content-Type": "application/json",
Authorization: `Bearer ${process.env.PAYMENT_KEY}`,
},
body: JSON.stringify({ amount }),
signal: controller.signal,
})
if (!res.ok) {
throw new Error(`Payment API error: ${res.status}`)
}
return await res.json()
} finally {
clearTimeout(timeout)
}
}Tiga hal yang wajib: timeout (API eksternal bisa hang), env var untuk secrets (bukan hardcode), dan cek res.ok (jangan abaikan status error).
Selesaikan praktik dengan dua langkah:
src/app/api/notes/route.ts dan src/app/api/notes/[id]/route.ts sesuai pola di atas.curl dari terminal:curl -s http://localhost:3000/api/notes
curl -s -X POST http://localhost:3000/api/notes \
-H "Content-Type: application/json" \
-d '{"title":"Dari curl","body":"Isi catatan"}'
curl -s http://localhost:3000/api/notes/not-existResponse ketiga harus berstatus 404 dengan { "error": "Catatan tidak ditemukan" } — bukti kontrak error kalian bekerja.
Inti yang harus dibawa pulang:
res.ok.Di episode 6 selanjutnya kita membahas topik yang menentukan aplikasi kalian berharga atau tidak: auth fullstack — session, JWT, middleware, dan OAuth dengan Auth.js — plus praktik login/register penuh. Sampai jumpa di episode 6!