Kenapa ini penting
Sampai sekarang, setiap file page.js yang kamu buat menghasilkan halaman HTML untuk dibaca manusia. Tapi aplikasi sungguhan sering perlu melayani "pembaca" yang bukan manusia: aplikasi mobile yang butuh data JSON, layanan pembayaran yang mengirim notifikasi (webhook) setelah pelanggan membayar, bot Telegram, atau sistem lain milik rekan kerjamu. Mereka tidak butuh tombol dan warna, mereka butuh data mentah lewat HTTP.
Di sinilah Route Handler berperan. Route Handler adalah cara Next.js membuat endpoint API (alamat URL yang menjawab dengan data, bukan halaman) di dalam folder app/. Dengan Route Handler, proyek Next.js-mu sekaligus menjadi backend. Kamu tidak perlu membuat server Express terpisah hanya untuk beberapa endpoint.
Analogi: kalau page.js itu etalase toko yang ditata cantik untuk pengunjung, Route Handler itu pintu belakang untuk kurir. Kurir tidak peduli etalase; dia datang membawa atau mengambil paket (data) dengan format yang sudah disepakati.
Konsep inti
File route.js dan method HTTP
Route Handler dibuat dengan file bernama route.js di dalam folder app/. Lokasinya menentukan URL, persis seperti page.js. File app/api/posts/route.js melayani URL /api/posts. Awalan api/ bukan aturan wajib, hanya kebiasaan supaya endpoint data mudah dibedakan dari halaman.
Di dalam file itu, kamu meng-export function yang namanya sama dengan method HTTP: GET, POST, PUT, PATCH, DELETE, HEAD, dan OPTIONS. Method HTTP adalah "kata kerja" sebuah request: GET untuk mengambil, POST untuk membuat, PATCH/PUT untuk mengubah, DELETE untuk menghapus. Kalau ada yang memanggil method yang tidak kamu export, Next.js otomatis menjawab 405 Method Not Allowed.
Satu aturan penting: route.js tidak boleh berada di folder yang sama dengan page.js. Keduanya sama-sama "mengambil alih" URL itu, jadi akan bentrok. app/page.js dan app/api/route.js aman karena foldernya berbeda.
Request dan Response standar Web
Function handler menerima objek Request dan harus mengembalikan objek Response. Keduanya bukan buatan Next.js, melainkan Web API standar yang juga ada di browser dan di fetch(). Artinya pengetahuanmu tentang fetch langsung terpakai:
await request.json()membaca body JSON.await request.formData()membaca body form.request.headers.get('nama-header')membaca header.Response.json(data, { status: 201 })membuat response JSON dengan status code tertentu.
Next.js juga menyediakan versi "plus": NextRequest (punya request.nextUrl.searchParams dan request.cookies) serta NextResponse (punya helper redirect, rewrite, dan pengaturan cookie). Kamu bisa memakai Request biasa untuk hampir semua kasus, lalu naik ke NextRequest saat butuh fitur tambahannya.
Parameter dinamis (params)
Untuk URL seperti /api/posts/halo-dunia, buat folder dinamis app/api/posts/[slug]/route.js. Handler menerima argumen kedua berisi params. Sejak Next.js 15, params adalah Promise, jadi harus di-await: const { slug } = await params. Lupa await adalah kesalahan nomor satu di versi baru.
Caching Route Handler di Next.js 16
Dengan Cache Components aktif (default proyek baru), GET Route Handler mengikuti aturan yang sama dengan halaman. Kalau handler tidak membaca data request (request.url, header, cookie) dan tidak memanggil data yang belum di-cache, hasilnya bisa diprerender saat build. Begitu handler membaca hal spesifik request, misalnya request.headers atau params yang tidak dikenal saat build, ia berjalan setiap kali ada request. Kalau butuh data database tapi tetap ingin cepat, taruh query di function terpisah yang memakai 'use cache' (Modul 4 dan 8). Method selain GET tidak pernah di-cache.
Kapan Route Handler, kapan Server Action?
Pertanyaan ini akan terus muncul, karena Modul 2 membahas Server Actions yang juga bisa menulis data. Pegangannya sederhana:
| Kebutuhan | Pilih |
|---|---|
| Form dan tombol di aplikasi Next.js-mu sendiri | Server Action |
| Endpoint untuk aplikasi lain (mobile, pihak ketiga) | Route Handler |
| Menerima webhook (Midtrans, GitHub, Stripe) | Route Handler |
Butuh URL yang stabil dan bisa dipanggil dengan curl |
Route Handler |
| Mengirim file, RSS, CSV, atau format non-HTML | Route Handler |
| Mutasi data dari UI lalu langsung menampilkan halaman terbaru | Server Action |
Server Action itu "jalur internal" yang dirancang untuk UI React-mu. Route Handler itu "loket publik" dengan alamat jelas yang bisa dipakai siapa pun yang tahu URL-nya.
Contoh kode
Contoh pertama memakai data dalam memori (array biasa) supaya kamu fokus ke mekanisme HTTP dulu. Database menyusul di Modul 4.
// app/api/notes/route.js
// Data sementara di memori: hilang saat server restart (cukup untuk belajar)
const notes = [
{ id: 1, text: 'Belajar Route Handler' },
{ id: 2, text: 'Pahami Request & Response' },
]
// GET /api/notes?q=route → cari catatan
export async function GET(request) {
const { searchParams } = new URL(request.url)
const q = searchParams.get('q')?.toLowerCase() ?? ''
const hasil = notes.filter((n) => n.text.toLowerCase().includes(q))
return Response.json({ count: hasil.length, notes: hasil })
}
// → {"count":1,"notes":[{"id":1,"text":"Belajar Route Handler"}]}
// POST /api/notes body: {"text":"Catatan baru"}
export async function POST(request) {
const body = await request.json().catch(() => null) // body rusak → null
if (!body || typeof body.text !== 'string' || body.text.trim() === '') {
return Response.json({ error: 'Field text wajib diisi' }, { status: 400 })
}
const note = { id: Date.now(), text: body.text.trim() }
notes.push(note)
return Response.json(note, { status: 201 }) // 201 = Created
}
Coba panggil dari terminal saat npm run dev berjalan:
curl "http://localhost:3000/api/notes?q=route"
# → {"count":1,"notes":[{"id":1,"text":"Belajar Route Handler"}]}
curl -X POST http://localhost:3000/api/notes \
-H "Content-Type: application/json" \
-d '{"text":"Catatan baru"}'
# → {"id":1793000000000,"text":"Catatan baru"}
curl -X DELETE http://localhost:3000/api/notes -i
# → HTTP/1.1 405 Method Not Allowed (DELETE tidak di-export)
Contoh kedua diambil dari proyek akhir: endpoint dengan parameter dinamis yang membaca database lewat fungsi getPublishedPost (dibuat di Modul 4).
// app/api/posts/[slug]/route.js
import { getPublishedPost } from '@/lib/dal'
// GET /api/posts/halo-dunia
export async function GET(request, { params }) {
const { slug } = await params // params berupa Promise sejak Next.js 15
const post = await getPublishedPost(slug)
if (!post) {
return Response.json({ error: 'Artikel tidak ditemukan' }, { status: 404 })
}
return Response.json(post)
}
// curl localhost:3000/api/posts/halo-dunia → {"id":1,"slug":"halo-dunia","title":"Halo Dunia",...}
// curl localhost:3000/api/posts/nggak-ada → 404 {"error":"Artikel tidak ditemukan"}
Contoh ketiga adalah endpoint webhook sederhana yang dilindungi secret di header. Endpoint ini dipanggil CMS atau script lain untuk menyegarkan cache, dan memakai after() untuk mencatat log setelah response terkirim, sehingga pemanggil tidak perlu menunggu.
// app/api/revalidate/route.js
import { revalidateTag } from 'next/cache'
import { after } from 'next/server'
export async function POST(request) {
// 1. Tolak siapa pun yang tidak membawa secret yang benar
if (request.headers.get('x-revalidate-secret') !== process.env.REVALIDATE_SECRET) {
return Response.json({ error: 'Unauthorized' }, { status: 401 })
}
// 2. Baca body (kalau kosong/rusak, pakai default)
const body = await request.json().catch(() => ({}))
const tag = typeof body.tag === 'string' ? body.tag : 'posts'
// 3. Tandai cache bertag ini basi (stale-while-revalidate)
revalidateTag(tag, 'max')
// 4. Kerjaan sampingan dijalankan SETELAH response dikirim
after(() => console.log(`[revalidate] tag=${tag}`))
return Response.json({ revalidated: true, tag })
}
// curl -X POST -H 'x-revalidate-secret: salah' localhost:3000/api/revalidate → 401
// dengan secret benar → {"revalidated":true,"tag":"posts"}
Kesalahan umum
- Menaruh
route.jsdi folder yang sama denganpage.js. Next.js akan menolak karena keduanya berebut URL yang sama. Pindahkan endpoint ke subfolder, misalnyaapp/api/.... - Lupa
await params. Di Next.js 15 ke atas,params.slugtanpaawaitmenghasilkanundefined. Selalu tulisconst { slug } = await params. - Memakai Route Handler untuk form internal. Membuat
POST /api/postslalu memanggilnya denganfetchdari Client Component memang bisa, tapi kamu harus mengurus loading, error, dan refresh halaman sendiri. Untuk form di aplikasimu sendiri, Server Action (Modul 2) jauh lebih ringkas. - Tidak memvalidasi input. Body request bisa berisi apa saja, termasuk JSON rusak. Bungkus
request.json()dengan.catch(), cek tipe data, dan kembalikan status400dengan pesan jelas. - Endpoint sensitif tanpa pengecekan. URL Route Handler bisa ditebak dan dipanggil siapa pun. Endpoint yang mengubah data wajib mengecek secret, session, atau tanda tangan webhook.
Latihan
- Mudah, Buat
app/api/hello/route.jsyang menjawabGETdengan{"pesan":"Halo, <nama>!"}, di mana<nama>diambil dari query?nama=. Kalau query kosong, pakai "teman". - Sedang, Lanjutkan contoh
notes: buatapp/api/notes/[id]/route.jsdenganGET(ambil satu catatan) danDELETE(hapus). Kembalikan404bila id tidak ada. - Menantang, Buat endpoint
GET /api/notes.csv(folderapp/api/notes.csv/route.js) yang mengembalikan daftar catatan dalam format CSV, lengkap dengan headerContent-Type: text/csvsupaya browser mengunduhnya sebagai file.
Lihat solusi
// 1. app/api/hello/route.js
export async function GET(request) {
const nama = new URL(request.url).searchParams.get('nama') || 'teman'
return Response.json({ pesan: `Halo, ${nama}!` })
}
// /api/hello?nama=Sari → {"pesan":"Halo, Sari!"}
// 2. app/api/notes/[id]/route.js
// Agar berbagi data dengan route.js induk, pindahkan array notes ke lib/notes.js lalu import.
import { notes } from '@/lib/notes'
export async function GET(request, { params }) {
const { id } = await params
const note = notes.find((n) => n.id === Number(id))
if (!note) return Response.json({ error: 'Tidak ditemukan' }, { status: 404 })
return Response.json(note)
}
export async function DELETE(request, { params }) {
const { id } = await params
const index = notes.findIndex((n) => n.id === Number(id))
if (index === -1) return Response.json({ error: 'Tidak ditemukan' }, { status: 404 })
notes.splice(index, 1)
return new Response(null, { status: 204 }) // 204 = sukses tanpa isi
}
// 3. app/api/notes.csv/route.js
import { notes } from '@/lib/notes'
export async function GET() {
const baris = notes.map((n) => `${n.id},"${n.text.replaceAll('"', '""')}"`)
const csv = ['id,text', ...baris].join('\n')
return new Response(csv, {
headers: {
'Content-Type': 'text/csv; charset=utf-8',
'Content-Disposition': 'attachment; filename="notes.csv"',
},
})
}
// Catatan: GET ini tidak membaca data request, jadi saat build ia diprerender (○ Static).
// Catatan yang ditambah lewat POST tidak akan muncul di CSV produksi. Kalau CSV harus
// selalu segar, tambahkan `await connection()` (import dari 'next/server') di awal GET.
Ringkasan
- Route Handler = file
route.jsdiapp/yang meng-export function bernama method HTTP (GET,POST, dan seterusnya). - Handler menerima
Requestdan mengembalikanResponsestandar Web;Response.json()paling sering dipakai. paramsdi route dinamis adalah Promise, wajib di-await.GETtanpa data request bisa diprerender; method lain selalu jalan per request.- Pakai Route Handler untuk API publik, webhook, dan format non-HTML; pakai Server Action untuk form di aplikasimu sendiri.