kodingup.camp

Beranda/Next.js Mastery: Full-Stack, Auth & Deploy

Route Handler

Modul 1 dari 115 menit bacaRoute Handlers

Isi modul

    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:

    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

    1. Menaruh route.js di folder yang sama dengan page.js. Next.js akan menolak karena keduanya berebut URL yang sama. Pindahkan endpoint ke subfolder, misalnya app/api/....
    2. Lupa await params. Di Next.js 15 ke atas, params.slug tanpa await menghasilkan undefined. Selalu tulis const { slug } = await params.
    3. Memakai Route Handler untuk form internal. Membuat POST /api/posts lalu memanggilnya dengan fetch dari 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.
    4. Tidak memvalidasi input. Body request bisa berisi apa saja, termasuk JSON rusak. Bungkus request.json() dengan .catch(), cek tipe data, dan kembalikan status 400 dengan pesan jelas.
    5. 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

    1. Mudah, Buat app/api/hello/route.js yang menjawab GET dengan {"pesan":"Halo, <nama>!"}, di mana <nama> diambil dari query ?nama=. Kalau query kosong, pakai "teman".
    2. Sedang, Lanjutkan contoh notes: buat app/api/notes/[id]/route.js dengan GET (ambil satu catatan) dan DELETE (hapus). Kembalikan 404 bila id tidak ada.
    3. Menantang, Buat endpoint GET /api/notes.csv (folder app/api/notes.csv/route.js) yang mengembalikan daftar catatan dalam format CSV, lengkap dengan header Content-Type: text/csv supaya 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

    Video pendamping

    Kalau penjelasan di atas belum masuk, tonton ini. Video dari kanal YouTube pihak lain.

    Next.js 15 Tutorial - 35 - Route HandlersCodevolution · Bahasa InggrisBuka di YouTube

    Kuis pemahaman

    Pilih satu jawaban per soal. Penjelasan muncul setelah kamu menjawab.