Next.js Route Handlers
Definisi
Route Handler di Next.js App Router adalah file route.ts atau route.js yang mengekspor function HTTP method seperti GET, POST, PUT, PATCH, DELETE, atau OPTIONS untuk melayani request di dalam folder app/.
Kalau page dipakai untuk merender UI, route handler dipakai untuk membentuk endpoint. Isinya bukan komponen React, melainkan logika request/response yang bekerja dengan Request, Response, atau NextResponse.
Topik yang dibahas di halaman ini:
- kapan route handler lebih tepat daripada page atau Server Action
- bagaimana request dibaca dari
URL, query string, headers, dan body - bagaimana response dibentuk dengan
Responseatau helper Next.js - jebakan umum saat membangun endpoint di App Router
Kapan route handler dipakai
Route Handler dipakai saat kamu butuh endpoint HTTP yang eksplisit. Contoh paling umum:
- API untuk client component atau aplikasi lain
- webhook dari payment gateway, GitHub, Stripe, atau service eksternal lain
- endpoint internal untuk integrasi yang tidak cocok diekspos sebagai page
- proxy ke backend lain agar secret tetap di server
- custom response seperti JSON, file download, redirect, atau status code tertentu
- endpoint
GETyang perlu dibaca oleh browser,fetch, atau tool eksternal
Pakai route handler kalau yang kamu butuhkan adalah kontrak HTTP. Jangan pakai page kalau tujuan utamanya bukan UI.
Kapan tidak perlu route handler
Route handler bukan jawaban untuk semua kasus. Kadang opsi lain lebih pas:
- Page / Server Component → kalau hasil akhirnya adalah UI
- Server Action → kalau action berasal dari form atau interaksi UI di aplikasi yang sama
- Client fetch langsung ke backend → kalau backend memang publik dan tidak perlu disembunyikan
Aturan praktisnya:
- baca data untuk ditampilkan → seringnya page / Server Component
- ubah data dari UI yang sama → seringnya Server Action
- layani request HTTP generik → route handler
- terima request dari luar aplikasi → route handler
Struktur file dan routing
Route Handler hidup di folder app/ dan mengikuti pola route segment.
app/
api/
posts/
route.ts
posts/
[id]/
route.tsContoh hasil URL:
app/api/posts/route.ts→/api/postsapp/api/posts/[id]/route.ts→/api/posts/123
Kalau folder berisi page.tsx dan route.ts, keduanya punya peran berbeda:
page.tsxmenangani render UIroute.tsmenangani request HTTP ke endpoint itu
Biasanya satu path tidak dipakai untuk page dan endpoint sekaligus kalau bisa dihindari, supaya maksudnya jelas.
Cara kerja request di Route Handler
Di Route Handler, request datang sebagai objek Request standar Web API. Itu artinya kamu tidak membaca req.query atau req.body ala framework lama; kamu membaca input melalui API request modern.
Sumber data yang sering dipakai:
request.url→ untuk mengakses URL penuhnew URL(request.url)→ untuk parsing pathname dan query stringrequest.headers→ untuk header sepertiauthorization,content-type, ataux-signatureawait request.json()→ untuk body JSONawait request.text()→ untuk payload text atau signature verificationawait request.formData()→ untuk form submission atau multipart/form-data
Query string
Query string dibaca dari URL.
export async function GET(request: Request) {
const url = new URL(request.url)
const page = Number(url.searchParams.get('page') ?? '1')
const q = url.searchParams.get('q') ?? ''
return Response.json({ page, q })
}Poin penting:
- gunakan
new URL(request.url)untuk parsing yang aman searchParams.get()mengembalikanstring | null- semua nilai query pada dasarnya string, jadi angka dan boolean perlu dikonversi sendiri
- jangan lupa default value kalau parameter opsional
Dynamic segment
Parameter path dibaca dari argumen kedua handler.
export async function GET(
request: Request,
{ params }: { params: Promise<{ id: string }> }
) {
const { id } = await params
return Response.json({ id })
}Di App Router modern, params bisa berupa promise pada beberapa konteks. Karena itu aman untuk await params sebelum dipakai.
Body JSON
Kalau request mengirim JSON, body dibaca sekali lalu diparse.
export async function POST(request: Request) {
const body = await request.json()
const title = String(body.title ?? '')
return Response.json({ title })
}Hal yang perlu diingat:
request.json()hanya bisa dibaca sekali- kalau body invalid JSON, parsing akan gagal dan melempar error
- validasi tetap harus dilakukan di server, jangan percaya shape body dari client
- cek
content-typekalau endpoint bisa menerima beberapa format body
FormData
Kalau request berasal dari form atau upload, gunakan formData().
export async function POST(request: Request) {
const formData = await request.formData()
const name = String(formData.get('name') ?? '')
return Response.json({ name })
}Pola ini cocok untuk:
- form HTML biasa
- multipart form upload
- request yang lebih natural diperlakukan sebagai key-value pair
Response helpers
Route Handler bisa mengembalikan Response standar Web API atau helper dari Next.js seperti NextResponse.
Response.json()
Untuk JSON API, ini biasanya pilihan paling simpel.
export async function GET() {
return Response.json({ ok: true })
}Kelebihannya:
- ringkas
- standar Web API
- cocok untuk endpoint JSON modern
new Response()
Pakai ini kalau kamu butuh kontrol lebih langsung.
export async function GET() {
return new Response('hello', {
status: 200,
headers: {
'content-type': 'text/plain; charset=utf-8',
},
})
}Cocok untuk:
- text response
- stream
- file download
- response non-JSON
NextResponse
NextResponse dipakai saat kamu butuh helper Next.js seperti cookies, redirect, atau manipulasi response yang lebih spesifik.
import { NextResponse } from 'next/server'
export function GET() {
return NextResponse.json({ ok: true })
}Atau untuk redirect:
import { NextResponse } from 'next/server'
export function GET() {
return NextResponse.redirect(new URL('/login', 'https://example.com'))
}Gunakan NextResponse kalau memang butuh fitur Next.js. Kalau cukup standar Web API, Response sering lebih bersih.
Alur request → handler → response
Inti Route Handler adalah alur yang sangat deterministik: request masuk, dibaca, divalidasi, diproses, lalu dibalas.
Kalau endpoint sudah dipahami lewat flow ini, debugging biasanya jadi lebih cepat. Yang perlu dicek tinggal di tahap mana request berhenti.
Contoh endpoint lengkap
import { NextResponse } from 'next/server'
type Params = {
params: Promise<{ id: string }>
}
export async function GET(request: Request, { params }: Params) {
const { id } = await params
const url = new URL(request.url)
const verbose = url.searchParams.get('verbose') === 'true'
if (!id) {
return NextResponse.json({ error: 'id wajib diisi' }, { status: 400 })
}
return NextResponse.json({
id,
verbose,
source: 'route-handler',
})
}
export async function POST(request: Request) {
const body = await request.json().catch(() => null)
if (!body || typeof body !== 'object') {
return NextResponse.json({ error: 'body JSON tidak valid' }, { status: 400 })
}
const title = String((body as { title?: unknown }).title ?? '').trim()
if (!title) {
return NextResponse.json({ error: 'title wajib diisi' }, { status: 422 })
}
return NextResponse.json(
{
ok: true,
title,
},
{ status: 201 }
)
}Hal yang ditunjukkan contoh ini:
GETdanPOSTbisa hidup dalam file yang sama- query string dibaca lewat
URLSearchParams paramsdipakai untuk path dinamis- body JSON harus divalidasi manual
- response bisa diberi status code yang spesifik
Kapan response helper dipilih
Panduan cepatnya begini:
Response.json()→ endpoint JSON biasanew Response()→ text, stream, file, atau response customNextResponse.json()→ JSON dengan utilitas Next.jsNextResponse.redirect()→ redirect dari endpointNextResponse.rewrite()→ rewrite path internal bila benar-benar perlu
Kalau tim ingin endpoint yang terasa seperti API modern, Response.json() sering jadi default yang paling bersih.
Jebakan umum pada endpoint di App Router
1. Menganggap body bisa dibaca berkali-kali
Body request adalah stream. Setelah request.json() atau request.text() dipanggil, body itu tidak bisa dipakai ulang begitu saja.
Solusinya:
- baca body sekali
- simpan hasil parse di variabel lokal
- kalau perlu format lain, ambil dari hasil parse, bukan dari stream yang sama
2. Lupa bahwa query string selalu string
?page=2 tetap string sampai kamu ubah sendiri menjadi angka.
Kalau ini diabaikan:
- perbandingan angka bisa salah
- validasi jadi bocor
- pagination mudah error
3. Tidak memvalidasi content-type
Endpoint yang berharap JSON harus memastikan request memang mengirim JSON.
Kalau tidak:
- form submit bisa bikin parsing salah
- webhook bisa gagal dibaca
- payload aneh bisa lolos terlalu jauh
4. Mengembalikan object biasa
Route Handler harus mengembalikan Response atau turunan yang valid. Mengembalikan object mentah bukan response HTTP.
5. Mencampur Route Handler dengan Server Action tanpa batas yang jelas
Keduanya sama-sama berjalan di server, tapi tujuan dan kontraknya beda.
- Route Handler = endpoint HTTP
- Server Action = aksi server yang dipanggil dari aplikasi Next.js
Kalau batas ini kabur, debugging auth, caching, dan shape request jadi lebih sulit.
6. Menaruh logic bisnis terlalu banyak di file route
Route Handler sebaiknya menjadi lapisan transport, bukan tempat semua aturan bisnis ditumpuk.
Pola yang lebih sehat:
- route handler menangani parsing request, status code, response
- service layer menangani logika bisnis
- repository atau data access layer menangani query database
7. Salah mengira route handler selalu statis
Endpoint bisa saja dinamis, terutama kalau:
- membaca cookies atau headers
- memakai request-specific data
- melakukan operasi yang tidak bisa di-cache
Jangan asumsi semua endpoint mendapat caching yang sama.
8. Tidak konsisten membedakan error client dan error server
Gunakan kode status yang masuk akal:
400untuk input tidak valid401untuk belum autentikasi403untuk tidak berhak404untuk resource tidak ada422untuk validasi semantik gagal500untuk error server tak terduga
Kalau semua error dibalas 500, diagnosa production jadi lebih susah.
Alur produksi yang sehat
Pisahkan tanggung jawab seperti ini supaya file route.ts tetap tipis dan endpoint lebih gampang diuji.
Catatan implementasi
- simpan parsing input di awal handler
- validasi sebelum menyentuh database
- pilih status code yang sesuai dengan jenis gagalannya
- gunakan helper response yang paling sederhana untuk kasus tersebut
- kalau endpoint dipakai publik, pikirkan rate limit, auth, dan audit log
- kalau endpoint menerima webhook, verifikasi signature sebelum memproses payload
Referensi terkait
- Next.js App Router routing dan data fetching
- Metadata, SEO, route groups, layouts, loading/error UI
- Caching, revalidation, dan mutation flow