Pendahuluan: Kenapa Middleware Berganti Nama Jadi Proxy?
Kalau Anda sudah mengikuti seri panduan kami — mulai dari fitur-fitur utama Next.js 16, Server Actions, Data Fetching & Caching, Autentikasi & Otorisasi, hingga Route Handlers — pasti sadar ada satu perubahan besar yang belum kita bahas: penggantian nama middleware.ts menjadi proxy.ts.
Dan ini bukan sekadar rename kosmetik. Serius.
Selama bertahun-tahun, istilah "middleware" di Next.js sering bikin bingung — terutama developer yang punya background Express.js. Di Express, middleware itu bisa melakukan apa saja: baca body request, query database, validasi JWT lengkap, bahkan nulis ke file system. Tapi middleware di Next.js nggak pernah dimaksudkan untuk semua itu. Middleware Next.js berjalan di network boundary — lapisan paling depan yang mencegat request sebelum sampai ke aplikasi Anda. Fungsinya lebih mirip reverse proxy: redirect, rewrite, manipulasi header, dan pengecekan ringan.
Jadi, dengan mengganti namanya menjadi Proxy, Next.js akhirnya membuat tujuan fitur ini jauh lebih jelas. Jujur, ini perubahan yang sudah lama ditunggu. Dan bukan hanya namanya yang berubah — runtime default-nya juga berpindah dari Edge Runtime ke Node.js Runtime, yang artinya Anda sekarang punya akses penuh ke API Node.js. Tidak ada lagi frustrasi karena batasan Edge Runtime yang bikin pusing.
Dalam panduan ini, kita akan bahas semuanya: cara migrasi dari middleware.ts ke proxy.ts, pola-pola umum yang bisa langsung Anda terapkan (autentikasi, CORS, rate limiting, A/B testing), pelajaran keamanan dari CVE-2025-29927, sampai cara unit test proxy Anda. Yuk, langsung mulai.
Apa yang Berubah: middleware.ts vs proxy.ts
Sebelum masuk ke kode, mari kita lihat perbandingan langsung antara konvensi lama dan baru. Tabel ini harusnya cukup menjelaskan:
| Aspek | middleware.ts (Next.js 15 ke bawah) | proxy.ts (Next.js 16) |
|---|---|---|
| Nama file | middleware.ts | proxy.ts |
| Nama fungsi ekspor | middleware() | proxy() |
| Runtime default | Edge Runtime | Node.js Runtime |
| Akses Node.js API | Terbatas | Penuh |
| Status | Deprecated | Aktif (recommended) |
| Edge Runtime support | Ya (default) | Tidak didukung |
Perubahan runtime ini punya implikasi yang cukup penting. Dulu, karena middleware berjalan di Edge Runtime, Anda tidak bisa pakai modul Node.js bawaan seperti fs, path, atau crypto versi penuh. Sekarang dengan proxy.ts yang berjalan di Node.js Runtime, batasan itu hilang sepenuhnya.
Tapi ada catatan penting: Edge Runtime tidak didukung di proxy.ts. Kalau Anda masih butuh Edge Runtime untuk kasus tertentu, Anda harus tetap pakai file middleware.ts — meskipun sudah deprecated dan bakal dihapus di versi mendatang.
Cara Migrasi dari middleware.ts ke proxy.ts
Opsi 1: Codemod Otomatis (Direkomendasikan)
Next.js menyediakan codemod resmi yang bakal otomatis mengganti nama file dan fungsi untuk Anda:
npx @next/codemod@canary middleware-to-proxy .
Codemod ini melakukan dua hal sederhana: rename file dari middleware.ts jadi proxy.ts, dan ganti nama fungsi ekspor dari middleware jadi proxy. Logika di dalamnya? Tetap sama persis. Nggak ada yang berubah selain nama.
Opsi 2: Migrasi Manual
Kalau Anda tipe orang yang lebih suka kontrol penuh (saya bisa relate), migrasi manualnya sebenarnya cukup sederhana:
# Rename file
mv middleware.ts proxy.ts
Kemudian ubah nama fungsi ekspornya:
// SEBELUM (middleware.ts)
import { NextResponse } from 'next/server';
import type { NextRequest } from 'next/server';
export function middleware(request: NextRequest) {
return NextResponse.redirect(new URL('/home', request.url));
}
export const config = {
matcher: '/about/:path*',
};
// SESUDAH (proxy.ts)
import { NextResponse } from 'next/server';
import type { NextRequest } from 'next/server';
export function proxy(request: NextRequest) {
return NextResponse.redirect(new URL('/home', request.url));
}
export const config = {
matcher: '/about/:path*',
};
Yang perlu diperhatikan: API surface-nya identik. NextRequest, NextResponse, objek config dengan matcher — semuanya bekerja persis sama. Jadi ini lebih banyak soal rename daripada refactor yang serius.
Perhatian Khusus untuk Auth Libraries
Nah, ini bagian yang sering bikin developer kaget. Kalau Anda menggunakan library autentikasi seperti Auth.js (NextAuth), Supabase Auth, atau Clerk, ada satu hal yang perlu diperhatikan saat migrasi.
Karena proxy.ts sekarang berjalan di Node.js Runtime (bukan Edge), flow autentikasi yang bergantung pada Edge Runtime mungkin perlu penyesuaian. Secara khusus, waspadai "logout loop" bug — kondisi di mana user mencoba logout tapi cookie tidak pernah ter-clear karena proxy tidak meneruskan response header dengan benar. Frustrasi banget kalau kena ini.
Solusinya: pastikan setiap respons dari proxy yang memodifikasi cookie menggunakan NextResponse dengan benar dan meneruskan header Set-Cookie ke klien.
Penempatan File dan Organisasi Kode
File proxy.ts harus ditempatkan di root proyek, atau di dalam folder src/ jika Anda menggunakannya — pada level yang sama dengan folder app/ atau pages/.
# Tanpa folder src
my-project/
├── app/
├── proxy.ts <-- di sini
├── next.config.ts
└── package.json
# Dengan folder src
my-project/
├── src/
│ ├── app/
│ └── proxy.ts <-- di sini
├── next.config.ts
└── package.json
Meskipun cuma satu file proxy.ts yang didukung per proyek (ya, cuma satu), Anda tetap bisa mengorganisir logika proxy ke dalam modul terpisah dan mengimpornya. Ini cara yang saya rekomendasikan untuk proyek menengah ke atas:
// lib/proxy/auth.ts
import type { NextRequest } from 'next/server';
import { NextResponse } from 'next/server';
export function handleAuth(request: NextRequest) {
const token = request.cookies.get('auth_token');
if (!token) {
return NextResponse.redirect(new URL('/login', request.url));
}
return null; // lanjutkan ke handler berikutnya
}
// lib/proxy/cors.ts
import type { NextRequest } from 'next/server';
import { NextResponse } from 'next/server';
const allowedOrigins = ['https://app.example.com'];
export function handleCors(request: NextRequest) {
const origin = request.headers.get('origin') ?? '';
if (request.method === 'OPTIONS' && allowedOrigins.includes(origin)) {
return NextResponse.json({}, {
headers: {
'Access-Control-Allow-Origin': origin,
'Access-Control-Allow-Methods': 'GET, POST, PUT, DELETE, OPTIONS',
'Access-Control-Allow-Headers': 'Content-Type, Authorization',
},
});
}
return null;
}
// proxy.ts — file utama
import { NextResponse } from 'next/server';
import type { NextRequest } from 'next/server';
import { handleAuth } from './lib/proxy/auth';
import { handleCors } from './lib/proxy/cors';
export function proxy(request: NextRequest) {
// CORS check dulu
const corsResponse = handleCors(request);
if (corsResponse) return corsResponse;
// Auth check
const authResponse = handleAuth(request);
if (authResponse) return authResponse;
return NextResponse.next();
}
export const config = {
matcher: ['/((?!_next/static|_next/image|favicon.ico).*)'],
};
Pola ini menjaga file proxy.ts tetap bersih dan mudah di-scan, sementara logika masing-masing concern hidup di modulnya sendiri.
Memahami Matcher: Mengontrol Kapan Proxy Berjalan
Secara default, proxy akan dijalankan untuk setiap route di proyek Anda. Setiap. Single. Route. Ini bisa menambah latensi yang nggak perlu kalau proxy cuma melakukan pengecekan yang relevan untuk beberapa path saja. Jadi, gunakan matcher untuk membatasi scope-nya.
Pola Matcher yang Sering Dipakai
// Matcher sederhana — satu path
export const config = {
matcher: '/dashboard/:path*',
};
// Matcher multiple — beberapa path
export const config = {
matcher: ['/dashboard/:path*', '/api/:path*', '/admin/:path*'],
};
// Negative lookahead — semua path KECUALI yang dikecualikan
export const config = {
matcher: [
'/((?!api|_next/static|_next/image|favicon.ico|sitemap.xml|robots.txt).*)',
],
};
// Matcher kondisional — berdasarkan header atau cookie
export const config = {
matcher: [
{
source: '/api/:path*',
has: [{ type: 'header', key: 'Authorization' }],
},
{
source: '/dashboard/:path*',
missing: [{ type: 'cookie', key: 'session' }],
},
],
};
Tips penting yang sering terlupakan: nilai matcher harus berupa konstanta karena dianalisis secara statis saat build time. Kalau Anda coba masukin variabel dinamis, matcher-nya bakal diabaikan begitu saja — tanpa error message. Bisa bikin debugging jadi menyebalkan.
Pola-Pola Proxy yang Wajib Anda Ketahui
1. Autentikasi Ringan (Optimistic Check)
Proxy cocok banget untuk pengecekan autentikasi optimistic — cek apakah cookie atau token ada, bukan validasi penuh. Validasi sesungguhnya (decode JWT, query database) seharusnya dilakukan di Server Components atau Server Actions.
// proxy.ts
import { NextResponse } from 'next/server';
import type { NextRequest } from 'next/server';
const protectedPaths = ['/dashboard', '/settings', '/profile'];
export function proxy(request: NextRequest) {
const { pathname } = request.nextUrl;
const isProtected = protectedPaths.some((path) =>
pathname.startsWith(path)
);
if (isProtected) {
const token = request.cookies.get('session_token');
if (!token) {
const loginUrl = new URL('/login', request.url);
loginUrl.searchParams.set('callbackUrl', pathname);
return NextResponse.redirect(loginUrl);
}
}
return NextResponse.next();
}
export const config = {
matcher: ['/((?!_next/static|_next/image|favicon.ico).*)'],
};
Perhatikan: kita hanya mengecek apakah cookie ada, bukan apakah isinya valid. Ini disengaja — proxy harus cepat. Dia itu "penjaga gerbang" pertama, bukan hakim terakhir. Validasi sesungguhnya terjadi di lapisan berikutnya.
2. CORS untuk API Routes
Menangani CORS di proxy punya keuntungan besar: semua API route Anda otomatis dapat header CORS yang konsisten tanpa perlu menambahkannya satu per satu di setiap route handler. Bayangkan kalau punya 20 API route — nggak mau kan copy-paste CORS headers ke semuanya?
// proxy.ts
import { NextRequest, NextResponse } from 'next/server';
const allowedOrigins = ['https://app.example.com', 'https://admin.example.com'];
const corsHeaders = {
'Access-Control-Allow-Methods': 'GET, POST, PUT, DELETE, OPTIONS',
'Access-Control-Allow-Headers': 'Content-Type, Authorization',
'Access-Control-Max-Age': '86400',
};
export function proxy(request: NextRequest) {
if (!request.nextUrl.pathname.startsWith('/api/')) {
return NextResponse.next();
}
const origin = request.headers.get('origin') ?? '';
const isAllowed = allowedOrigins.includes(origin);
// Handle preflight
if (request.method === 'OPTIONS') {
return NextResponse.json({}, {
headers: {
...(isAllowed && { 'Access-Control-Allow-Origin': origin }),
...corsHeaders,
},
});
}
const response = NextResponse.next();
if (isAllowed) {
response.headers.set('Access-Control-Allow-Origin', origin);
}
Object.entries(corsHeaders).forEach(([key, value]) => {
response.headers.set(key, value);
});
return response;
}
export const config = {
matcher: '/api/:path*',
};
3. Rate Limiting Sederhana
Untuk rate limiting dasar, Anda bisa pakai in-memory store di proxy. Tapi — dan ini penting — pendekatan ini cuma cocok untuk development atau single-instance deployment. Di produksi dengan multiple instances, data rate limit nggak akan di-share antar instance. Untuk production, pakai Redis atau Upstash.
// proxy.ts
import { NextResponse } from 'next/server';
import type { NextRequest } from 'next/server';
const rateLimit = new Map();
const WINDOW_MS = 60 * 1000; // 1 menit
const MAX_REQUESTS = 60;
function getRateLimitResponse(ip: string): NextResponse | null {
const now = Date.now();
const record = rateLimit.get(ip);
if (!record || now > record.resetTime) {
rateLimit.set(ip, { count: 1, resetTime: now + WINDOW_MS });
return null;
}
if (record.count >= MAX_REQUESTS) {
return NextResponse.json(
{ error: 'Terlalu banyak request. Coba lagi nanti.' },
{
status: 429,
headers: {
'Retry-After': String(Math.ceil((record.resetTime - now) / 1000)),
'X-RateLimit-Limit': String(MAX_REQUESTS),
'X-RateLimit-Remaining': '0',
},
}
);
}
record.count++;
return null;
}
export function proxy(request: NextRequest) {
const xff = request.headers.get('x-forwarded-for');
const ip = (xff && xff.split(',')[0].trim()) || request.ip || 'unknown';
const rateLimitResponse = getRateLimitResponse(ip);
if (rateLimitResponse) return rateLimitResponse;
return NextResponse.next();
}
export const config = {
matcher: '/api/:path*',
};
4. A/B Testing dengan Cookie-Based Routing
Ini salah satu use case favorit saya untuk proxy. A/B testing di level proxy berarti user nggak akan pernah melihat flicker — mereka langsung dapat versi yang sesuai, tanpa client-side redirect yang mengganggu.
// proxy.ts
import { NextResponse } from 'next/server';
import type { NextRequest } from 'next/server';
export function proxy(request: NextRequest) {
const { pathname } = request.nextUrl;
if (pathname === '/pricing') {
let variant = request.cookies.get('ab_pricing')?.value;
if (!variant) {
variant = Math.random() < 0.5 ? 'control' : 'experiment';
const response = NextResponse.rewrite(
new URL(variant === 'experiment' ? '/pricing-b' : '/pricing', request.url)
);
response.cookies.set('ab_pricing', variant, {
maxAge: 60 * 60 * 24 * 30, // 30 hari
httpOnly: true,
});
return response;
}
if (variant === 'experiment') {
return NextResponse.rewrite(new URL('/pricing-b', request.url));
}
}
return NextResponse.next();
}
Detail penting: user akan melihat URL /pricing di browser mereka terlepas dari variasi yang ditampilkan — karena kita pakai rewrite, bukan redirect. URL-nya tetap bersih.
5. Security Headers
Proxy juga tempat yang tepat untuk menambahkan security headers ke semua response. Ini hal sederhana yang sering di-skip padahal impactnya lumayan besar untuk keamanan:
// proxy.ts
import { NextResponse } from 'next/server';
import type { NextRequest } from 'next/server';
export function proxy(request: NextRequest) {
const response = NextResponse.next();
response.headers.set('X-Frame-Options', 'DENY');
response.headers.set('X-Content-Type-Options', 'nosniff');
response.headers.set('Referrer-Policy', 'strict-origin-when-cross-origin');
response.headers.set('X-XSS-Protection', '1; mode=block');
response.headers.set(
'Permissions-Policy',
'camera=(), microphone=(), geolocation=()'
);
return response;
}
Pelajaran Keamanan: CVE-2025-29927
Oke, ini bagian yang serius. Nggak ada pembahasan tentang proxy/middleware Next.js yang lengkap tanpa membahas CVE-2025-29927 — kerentanan kritis dengan skor CVSS 9.1 yang ditemukan pada Maret 2025.
Apa yang Terjadi?
Next.js menggunakan header internal bernama x-middleware-subrequest untuk menandai bahwa sebuah request adalah subrequest internal — bukan request langsung dari klien. Tujuannya untuk mencegah loop rekursif saat middleware memanggil dirinya sendiri.
Masalahnya? Siapa pun bisa menambahkan header ini ke request mereka. Dengan menyisipkan x-middleware-subrequest dengan nilai path middleware yang benar (misalnya middleware atau src/middleware), penyerang bisa membuat Next.js melewatkan seluruh middleware. Termasuk semua pengecekan autentikasi dan otorisasi di dalamnya. Ngeri, kan?
Versi yang Terdampak
- Next.js 15.x di bawah 15.2.3
- Next.js 14.x di bawah 14.2.25
- Versi 11.1.4 hingga 13.5.6
Prinsip Defense in Depth
Pelajaran terbesar dari CVE-2025-29927 bukan cuma tentang satu bug — ini tentang prinsip arsitektur yang lebih luas:
- Jangan pernah mengandalkan proxy/middleware sebagai satu-satunya lapisan keamanan. Proxy itu optimistic check — lapisan pertama yang memfilter request yang jelas-jelas nggak sah. Tapi validasi sesungguhnya harus terjadi di dekat sumber data.
- Implementasikan autentikasi di setiap titik akses data. Server Components, Server Actions, dan Route Handlers — semuanya harus memverifikasi session secara independen. Ya, ini artinya ada duplikasi pengecekan, tapi itu memang disengaja.
- Gunakan Data Access Layer (DAL). Seperti yang sudah kita bahas di panduan autentikasi, DAL memastikan setiap query ke database melewati pengecekan otorisasi — nggak peduli siapa yang memanggil.
Kalau Anda pakai deployment self-hosted (bukan Vercel/Netlify), pastikan sudah upgrade ke versi yang dipatch. Deployment di Vercel dan Netlify otomatis terlindungi karena arsitektur edge mereka memfilter header internal ini sebelum sampai ke aplikasi.
Unit Testing Proxy (Eksperimental)
Mulai Next.js 15.1, paket next/experimental/testing/server menyediakan utilitas untuk unit test file proxy. Fitur ini masih eksperimental (perhatikan prefix unstable_ di nama fungsinya), tapi sudah cukup berguna untuk memastikan proxy cuma berjalan di path yang diinginkan.
// __tests__/proxy.test.ts
import { unstable_doesProxyMatch } from 'next/experimental/testing/server';
import { isRewrite, getRewrittenUrl } from 'next/experimental/testing/server';
import { NextRequest } from 'next/server';
import { proxy, config } from '../proxy';
describe('Proxy matcher', () => {
it('should match dashboard routes', () => {
expect(
unstable_doesProxyMatch({
config,
url: '/dashboard/settings',
})
).toBe(true);
});
it('should not match static assets', () => {
expect(
unstable_doesProxyMatch({
config,
url: '/_next/static/chunks/main.js',
})
).toBe(false);
});
});
describe('Proxy behavior', () => {
it('should redirect to login when no session cookie', async () => {
const request = new NextRequest('https://example.com/dashboard');
const response = await proxy(request);
expect(response?.status).toBe(307);
expect(response?.headers.get('location')).toContain('/login');
});
});
Dengan unit test, Anda bisa memverifikasi logika routing sebelum deploy ke produksi. Ini sangat membantu mengurangi risiko kesalahan konfigurasi matcher yang (tanpa disadari) bisa membuka celah keamanan atau memblokir user yang seharusnya punya akses.
Background Work dengan waitUntil
Proxy mendukung NextFetchEvent yang menyertakan method waitUntil(). Ini memungkinkan Anda menjalankan operasi background tanpa menunda respons ke user — semacam fire-and-forget tapi yang terkelola:
// proxy.ts
import { NextResponse } from 'next/server';
import type { NextFetchEvent, NextRequest } from 'next/server';
export function proxy(request: NextRequest, event: NextFetchEvent) {
// Kirim analytics di background — tidak blocking response
event.waitUntil(
fetch('https://analytics.example.com/track', {
method: 'POST',
body: JSON.stringify({
pathname: request.nextUrl.pathname,
userAgent: request.headers.get('user-agent'),
timestamp: new Date().toISOString(),
}),
})
);
return NextResponse.next();
}
Ini cocok banget untuk analytics, logging, atau operasi lain yang nggak perlu selesai sebelum response dikirim ke browser.
Execution Order: Di Mana Proxy Berada dalam Lifecycle Request
Memahami urutan eksekusi ini penting untuk debugging. Saya pernah menghabiskan waktu berjam-jam debugging redirect yang ternyata di-override oleh next.config.js — jangan sampai Anda mengalami hal yang sama. Berikut urutan lengkapnya:
headersdarinext.config.jsredirectsdarinext.config.js- Proxy (rewrites, redirects, dll.)
beforeFilesrewrites darinext.config.js- Filesystem routes (
public/,_next/static/,pages/,app/) afterFilesrewrites darinext.config.js- Dynamic routes (
/blog/[slug]) fallbackrewrites darinext.config.js
Proxy berjalan sebelum cached content dan route matching — itulah sebabnya dia cocok untuk pengecekan autentikasi, redirect berbasis geo, dan A/B testing. Kalau proxy bilang "redirect ke /login", route lain nggak sempat diproses.
Best Practice: Apa yang Boleh dan Tidak Boleh Dilakukan di Proxy
Yang Boleh (dan Direkomendasikan)
- Redirect user yang tidak terautentikasi ke halaman login
- Menambahkan security headers ke semua response
- Rewrite URL untuk A/B testing atau fitur eksperimental
- Menangani CORS untuk API routes
- Rate limiting ringan (dengan catatan: pakai Redis di produksi)
- Logging dan analytics via
waitUntil()
Yang Tidak Boleh
- Query database. Proxy bukan tempat untuk Prisma atau Drizzle. Simpan query-query itu di Server Components atau Route Handlers.
- Validasi JWT penuh. Cukup cek keberadaan token saja, jangan decode dan verify di sini.
- Operasi CPU-intensive. Meski sekarang berjalan di Node.js, proxy tetap harus cepat — ingat, setiap single request melewatinya.
- Mengandalkan proxy sebagai satu-satunya lapisan keamanan. CVE-2025-29927 sudah membuktikan kenapa ini ide yang buruk. Selalu implementasikan defense in depth.
- Import modul besar. Jaga proxy tetap ringan untuk menghindari cold start lambat, terutama di environment serverless.
FAQ
Apakah middleware.ts masih bisa dipakai di Next.js 16?
Ya, masih bisa — tapi untuk kasus penggunaan Edge Runtime saja. File ini sudah deprecated dan akan dihapus di versi mendatang. Sebaiknya segera migrasi ke proxy.ts pakai codemod resmi: npx @next/codemod@canary middleware-to-proxy .
Apa perbedaan utama antara proxy.ts dan middleware.ts?
Perbedaan paling signifikan ada di runtime: middleware.ts berjalan di Edge Runtime, proxy.ts berjalan di Node.js Runtime. Artinya proxy.ts punya akses penuh ke API Node.js, tapi nggak bisa di-deploy ke edge locations. API surface-nya (NextRequest, NextResponse, matcher) tetap identik — jadi transisi-nya relatif smooth.
Apakah proxy.ts aman untuk autentikasi?
Proxy cocok untuk optimistic check — cek keberadaan cookie/token dan redirect kalau nggak ada. Tapi jangan pernah mengandalkan proxy sebagai satu-satunya lapisan autentikasi. CVE-2025-29927 sudah membuktikan bahwa middleware/proxy bisa di-bypass. Selalu validasi session di dekat sumber data: Server Components, Server Actions, atau Data Access Layer.
Bagaimana cara menangani multiple concern di satu file proxy?
Pisahkan setiap concern ke modul terpisah (misalnya lib/proxy/auth.ts, lib/proxy/cors.ts) dan import ke file proxy.ts utama. Jalankan setiap handler secara berurutan — kalau satu handler return response, langsung return tanpa jalankan handler berikutnya. Pola chain-of-responsibility sederhana ini cukup efektif.
Bisakah proxy.ts mengakses database atau memanggil API eksternal?
Secara teknis bisa, karena sekarang jalan di Node.js Runtime. Tapi ini sangat tidak direkomendasikan. Proxy didesain sebagai lapisan ringan untuk redirect, rewrite, dan manipulasi header. Operasi berat seperti query database atau panggilan API yang lambat akan memperlambat semua request — karena setiap request melewati proxy. Pakai Server Components, Server Actions, atau Route Handlers untuk hal-hal seperti itu.