Ringkasan Utama
Cara membuat middleware Next.js: buat file middleware.ts di root proyek, sejajar folder app, lalu ekspor fungsi yang menerima request dan mengembalikan NextResponse. Batasi dengan matcher agar tidak jalan di file statis, pakai redirect untuk memindahkan URL, rewrite untuk menayangkan halaman lain di URL yang sama, dan uji semuanya dengan curl.
Kamu menambahkan halaman /dashboard, lalu sadar siapa pun yang mengetik alamatnya langsung bisa membukanya. Kamu ingin satu tempat yang memeriksa request sebelum halaman dirender. Begitu file middleware dibuat, ada tiga hal yang sering terjadi: tidak ada yang terjadi sama sekali, CSS dan gambar ikut terblokir, atau browser berputar-putar sampai muncul ERR_TOO_MANY_REDIRECTS.
Panduan ini membangun satu file middleware kecil dari nol di Next.js 15 (App Router): header kustom, matcher, redirect bersyarat, rewrite, dan cara mengujinya dengan curl. Setiap klaim tentang API sudah dicocokkan dengan dokumentasi Next.js yang tercantum di bagian sumber, dan perilaku matcher serta NextResponse saya jalankan langsung di Next.js 15.5.7. Pola matcher yang dipakai sama dengan middleware.ts di situs ini.
Mau paham konsepnya lewat simulasi?
Panduan ini fokus pada langkah dan error. Lesson gratis kami menjelaskan urutan eksekusi, content negotiation lewat header Accept, dan kenapa Vary penting, lengkap dengan kuis: Tutorial Middleware Next.js: Matcher, Rewrite, dan Accept. Selesaikan panduan ini dulu supaya kamu punya middleware yang berjalan, lalu lanjut ke lesson untuk melihat bagaimana situs ini memakainya.
Yang kamu butuhkan
Semua bisa dikerjakan di laptop, tanpa server dan tanpa biaya tambahan.
- Proyek Next.js 15 dengan App Router (folder app), misalnya hasil create-next-app. Panduan ini memakai TypeScript.
- Dev server yang berjalan lewat npm run dev, biasanya di http://localhost:3000.
- curl di terminal. Kita menguji tanpa membuka browser, karena middleware hanya soal status, header, dan lokasi.
- Tiga halaman kosong supaya tujuan redirect dan rewrite ada: app/login/page.tsx, app/dashboard/page.tsx, dan app/campaign/page.tsx.
Langkah 1: Buat middleware.ts pertama dan pasang satu header
Middleware adalah satu file bernama middleware.ts di root proyek, sejajar dengan folder app (atau di dalam src jika kamu memakainya). Isinya satu fungsi yang menerima request, dan boleh me-rewrite, me-redirect, mengubah header, atau langsung menjawab. NextResponse.next() berarti lanjutkan seperti biasa. Kita pasang satu header sebagai bukti bahwa fungsinya berjalan.
// middleware.ts
// Wrong: app/middleware.ts or app/lib/middleware.ts. Next.js never loads it.
// Right: the project root, at the same level as the app folder (or inside src/).
import { NextResponse, type NextRequest } from "next/server";
export function middleware(request: NextRequest) {
const response = NextResponse.next(); // continue as normal
response.headers.set("x-hello-from-middleware", "hello");
return response;
}
// In a second terminal, while "npm run dev" is running:
// curl -I http://localhost:3000/Output yang diharapkan: Respons berstatus 200 dan memuat baris x-hello-from-middleware: hello. Jika baris itu tidak ada, middleware belum terbaca: lihat baris pertama di tabel error.
Catatan versi: panduan ini memakai Next.js 15, tempat file-nya bernama middleware.ts. Di Next.js 16 konvensi itu diganti nama menjadi proxy dengan konsep yang sama; langkah migrasinya ada di Migrasi middleware.ts ke proxy.ts di Next.js 16. Dokumentasi juga menyebut fitur ini sebaiknya jadi pilihan terakhir, jadi pakai ketika next.config atau sebuah route tidak cukup.
File middleware.ts yang ditaruh di dalam folder app tidak akan pernah dijalankan. Letaknya harus sejajar dengan app, dan satu file hanya boleh mengekspor satu fungsi middleware.
Langkah 2: Batasi dengan matcher supaya file statis tidak ikut
Tanpa matcher, middleware berjalan pada setiap request, termasuk file statis, optimasi gambar, dan isi folder public. Begitu kamu menambah redirect, CSS dan gambar ikut terpental. Matcher adalah export config yang menentukan path mana yang boleh sampai ke fungsi.
// Add to the bottom of middleware.ts
export const config = {
// Wrong: only api and Next.js internals are excluded, so /next.svg and
// every other file in public/ still goes through the function.
// matcher: ["/((?!api|_next/static|_next/image).*)"],
// Right: also exclude any path that contains a dot (a file extension).
// The doubled backslash is one escaped dot inside a JavaScript string.
matcher: ["/((?!api|_next|.*\\..*).*)"],
// Alternative: list only the paths that need the function.
// matcher: ["/dashboard/:path*", "/promo"],
};
// Matcher values must be literal constants. A variable is ignored,
// because Next.js reads this export at build time.
// curl -I http://localhost:3000/api/hello
// curl -I http://localhost:3000/next.svg
// curl -I http://localhost:3000/Pola di bawah adalah negative lookahead: cocokkan semua path kecuali yang diawali api atau _next, dan kecuali path yang memuat titik. Klausa titik itulah yang menyingkirkan file seperti next.svg, robots.txt, dan sitemap.xml. Dua aturan dari dokumentasi: source harus diawali slash, dan nilai matcher harus konstanta karena dianalisis saat build, sehingga variabel diabaikan.
Output yang diharapkan: curl -I ke /api/hello dan ke /next.svg tidak lagi memuat x-hello-from-middleware, sedangkan halaman biasa seperti / tetap memuatnya.
Langkah 3: Redirect halaman yang butuh login
Redirect mengirim klien ke URL lain: browser berpindah dan alamatnya berubah. Kita redirect /dashboard ke /login bila cookie bernama session tidak ada, dan menitipkan alamat asal di query from supaya halaman login bisa mengembalikan pengguna. Contoh di dokumentasi membangun tujuannya dengan objek URL dari request.url.
// middleware.ts (replace the function body from step 1)
export function middleware(request: NextRequest) {
const { pathname } = request.nextUrl;
// Wrong: if (!request.cookies.has("session")) redirect for EVERY path.
// /login matches too, so it redirects to /login forever.
// Right: only guard the paths that need a login.
if (pathname.startsWith("/dashboard") && !request.cookies.has("session")) {
const loginUrl = new URL("/login", request.url);
loginUrl.searchParams.set("from", pathname); // remember where they were going
return NextResponse.redirect(loginUrl); // 307 by default
}
const response = NextResponse.next();
response.headers.set("x-hello-from-middleware", "hello");
return response;
}
// curl -i http://localhost:3000/dashboard
// curl -i -H "Cookie: session=abc" http://localhost:3000/dashboardKode ini sengaja hanya mengecek path yang diawali /dashboard. Itu mencegah loop: kalau semua path tanpa cookie diredirect ke /login, maka /login sendiri ikut diredirect ke /login tanpa akhir, dan browser menyerah dengan ERR_TOO_MANY_REDIRECTS. Status yang dipakai NextResponse.redirect secara default adalah 307, yaitu redirect sementara yang mempertahankan method dan body request.
Output yang diharapkan: Tanpa cookie, curl -i menampilkan HTTP 307 dan baris location yang menuju http://localhost:3000/login?from=%2Fdashboard. Dengan cookie session, tidak ada lagi 307 ke /login.
Cek cookie seperti ini hanya optimistic: cookie palsu bernama session juga lolos. Dokumentasi autentikasi Next.js menyarankan middleware hanya membaca cookie tanpa query database, dan menegaskan bahwa middleware tidak boleh menjadi satu-satunya pertahanan. Pemeriksaan sebenarnya dilakukan sedekat mungkin dengan data.
Langkah 4: Rewrite untuk menayangkan halaman lain di URL yang sama
Rewrite menampilkan isi sebuah URL sebagai pengganti URL yang diminta, dan alamat di browser tidak berubah. Di contoh ini, /promo menayangkan halaman /campaign. Berikut file middleware.ts lengkap sampai langkah ini.
// middleware.ts (the whole file so far)
import { NextResponse, type NextRequest } from "next/server";
export function middleware(request: NextRequest) {
const { pathname } = request.nextUrl;
if (pathname.startsWith("/dashboard") && !request.cookies.has("session")) {
const loginUrl = new URL("/login", request.url);
loginUrl.searchParams.set("from", pathname);
return NextResponse.redirect(loginUrl);
}
// Wrong: NextResponse.redirect(...) here. The address bar would change to /campaign.
// Right: rewrite. The visitor keeps /promo and gets the /campaign page.
if (pathname === "/promo") {
return NextResponse.rewrite(new URL("/campaign", request.url));
}
const response = NextResponse.next();
response.headers.set("x-hello-from-middleware", "hello");
return response;
}
export const config = {
matcher: ["/((?!api|_next|.*\\..*).*)"],
};
// curl -i http://localhost:3000/promoAturan memilihnya sederhana. Pakai redirect bila alamat memang harus berpindah, misalnya halaman dipindah atau pengguna perlu login. Pakai rewrite bila alamat harus tetap tetapi isinya berasal dari tempat lain. Situs ini memakai rewrite di middleware.ts untuk melayani versi Markdown di URL halaman yang sama bagi agen yang meminta text/markdown, tanpa lompatan redirect.
Output yang diharapkan: curl -i ke /promo menghasilkan HTTP 200 tanpa baris location, dan isinya halaman campaign. Bandingkan dengan /dashboard di langkah 3 yang menghasilkan 307.
Langkah 5: Titipkan header ke halaman dengan arah yang benar
Kadang middleware perlu menitipkan informasi ke halaman atau route di belakangnya. Ada dua cara yang mirip tetapi arahnya berlawanan. NextResponse.next dengan opsi request lalu headers meneruskan header ke atas, yaitu ke halaman, route, atau server action, dan tidak mengirimnya ke klien. NextResponse.next dengan opsi headers langsung mengirim header ke klien, dan dokumentasi menandainya sebagai praktik yang tidak baik karena bisa menimpa header seperti Content-Type yang dibutuhkan framework.
// Replace the last three lines of the function (NextResponse.next ... return response)
const requestHeaders = new Headers(request.headers);
requestHeaders.set("x-version", "123");
// Right: forward upstream. The page or route sees x-version; the client does not.
const response = NextResponse.next({
request: { headers: requestHeaders },
});
// Wrong: NextResponse.next({ headers: requestHeaders })
// That sends them to the CLIENT, and can override headers such as Content-Type.
// A header meant for the client is set on the response itself.
response.headers.set("x-hello-from-middleware", "hello");
return response;
// curl -I http://localhost:3000/Pemakaian nyata pola ini adalah menitipkan nonce untuk Content Security Policy; ceritanya ada di Content Security Policy di Next.js dengan nonce. Jangan menyalin semua header request. Buat subset dengan allow-list dan buang authorization serta cookie. Hindari header besar karena bisa memicu error 431 Request Header Fields Too Large.
Output yang diharapkan: curl -I ke sebuah halaman tidak menampilkan x-version di respons, karena header itu hanya diteruskan ke atas. Header x-hello-from-middleware tetap muncul karena kita memasangnya langsung di respons.
Langkah 6: Uji semuanya dengan curl
Hasil middleware hanyalah status, header, dan Location, jadi curl sudah cukup untuk mengujinya. Simpan perintah berikut sebagai skrip kecil dan jalankan setiap kali kamu mengubah matcher atau logika. Satu loop menguji lima jenis path sekaligus: halaman biasa, API, file statis, halaman yang diredirect, dan halaman yang di-rewrite.
# check-middleware.sh (run while "npm run dev" is up)
# -s silent, -o /dev/null drop the body, -D - print only the response headers.
for p in / /api/hello /next.svg /dashboard /promo; do
echo "== $p"
curl -s -o /dev/null -D - "http://localhost:3000$p" | grep -iE "^(HTTP|location|x-hello)"
done
# The passing branch of the redirect:
curl -s -o /dev/null -D - -H "Cookie: session=abc" http://localhost:3000/dashboard | grep -iE "^(HTTP|location)"Setelah itu ulangi untuk /dashboard dengan cookie, supaya cabang yang lolos juga terbukti.
Output yang diharapkan: Halaman / menghasilkan 200 dan x-hello. /api/hello dan /next.svg tidak memuat x-hello (statusnya mengikuti route atau file itu sendiri). /dashboard menghasilkan 307 dengan location ke /login?from=%2Fdashboard. /promo menghasilkan 200 tanpa location. Dengan cookie, /dashboard tidak lagi 307.
Ingin tes otomatis? Next.js menyediakan helper eksperimental di next/experimental/testing/server: unstable_doesMiddlewareMatch untuk memastikan matcher cocok atau tidak pada sebuah URL, serta isRewrite, getRewrittenUrl, dan getRedirectUrl untuk memeriksa hasilnya. Namanya berawalan unstable karena masih eksperimental, dan di dokumentasi Next.js 16 fungsi pertama bernama unstable_doesProxyMatch.
Error umum dan cara memperbaikinya
Delapan masalah yang paling sering membuat pemula buntu saat pertama kali membuat middleware di Next.js.
| Gejala | Penyebab | Perbaikan |
|---|---|---|
| Middleware tidak jalan: header x-hello-from-middleware tidak muncul | File berada di dalam folder app, namanya salah, atau fungsinya tidak diekspor. | Pindahkan ke root proyek sejajar app (atau ke src), beri nama middleware.ts, dan ekspor fungsi bernama middleware atau default. |
| CSS, gambar, atau file di public ikut kena redirect atau rewrite | Tidak ada matcher, atau polanya belum mengecualikan path berekstensi (misalnya hanya api dan _next/static). | Tambahkan config dengan negative lookahead yang mengecualikan api, _next, dan path yang berisi titik. |
| Browser menampilkan ERR_TOO_MANY_REDIRECTS | Tujuan redirect (misalnya /login) juga kena aturan redirect yang sama. | Batasi aturan ke path yang memang butuh (pathname.startsWith) atau persempit matcher supaya halaman tujuan tidak ikut. |
| Matcher seperti diabaikan | Nilainya variabel atau hasil perhitungan, padahal matcher dianalisis saat build, atau source tidak diawali slash. | Tulis matcher sebagai string konstanta literal yang diawali slash. |
| Alamat di browser berubah padahal ingin tetap | Memakai NextResponse.redirect. | Pakai NextResponse.rewrite supaya isi berasal dari URL lain tetapi alamat tetap. |
| Header dari middleware tidak terbaca di halaman, atau malah muncul di respons ke klien | Arahnya tertukar: opsi headers pada next mengirim ke klien, bukan ke halaman. | Pakai next dengan opsi request berisi headers untuk meneruskan ke atas, dan response.headers.set untuk header ke klien. |
| Halaman dilindungi, tetapi Server Action atau path yang dikecualikan matcher masih bisa diakses | Matcher yang mengecualikan sebuah path juga melewatkan Server Function di path itu, dan cek cookie hanya optimistic. | Verifikasi autentikasi dan otorisasi di dalam setiap Server Action dan Route Handler, sedekat mungkin dengan data. |
| Setelah pindah ke Next.js 16, dokumentasi menyebut konvensi middleware deprecated | Konvensi file middleware diganti nama menjadi proxy. | Jalankan codemod resmi middleware-to-proxy lewat npx @next/codemod@canary di folder proyek. Ia mengganti nama file dan nama fungsinya. |
Checklist sebelum deploy
- File bernama middleware.ts dan sejajar folder app (di Next.js 16 menjadi proxy.ts).
- Matcher memakai konstanta dan mengecualikan api, _next, serta path berekstensi.
- Redirect hanya untuk path yang butuh, dan halaman tujuannya tidak ikut diredirect.
- Cek cookie dianggap optimistic; otorisasi sebenarnya ada di dekat data dan di setiap Server Action.
- Header ke halaman diteruskan lewat opsi request, dan tidak semua header request disalin.
- Semua kasus sudah diuji dengan curl: halaman biasa, API, file statis, redirect, dan rewrite.
Belajar lebih dalam
Sekarang kamu punya middleware.ts yang menambah header, memilih request lewat matcher, me-redirect, me-rewrite, dan bisa diuji dengan curl. Untuk memahami kenapa urutan eksekusinya begitu, bagaimana situs ini membaca header Accept untuk melayani Markdown di URL yang sama, dan kenapa Vary: Accept penting, lanjutkan ke lesson gratis berikut.
Tutorial Middleware Next.js: Matcher, Rewrite, dan Accept
Lesson itu membedah middleware.ts situs ini baris demi baris, dengan simulasi interaktif dan kuis di akhir.
Sumber
- Dokumentasi Next.js: proxy.js (konvensi middleware yang diganti nama): matcher, urutan eksekusi, header
- Dokumentasi Next.js: NextResponse (redirect, rewrite, next)
- Dokumentasi Next.js: cara menangani redirect
- Dokumentasi Next.js: autentikasi dan pemeriksaan optimistic di Proxy
- MDN: status 307 Temporary Redirect

