從 Next.js 的 Proxy 跟 Middleware 來理解請求的生命週期

前陣子在升級 Next.js 16 的時候踩到一些坑,發現 middleware.ts 被改名成 proxy.ts 了,然後 Edge Runtime 也不支援了,搞得我一臉問號。查了一下資料順便把 Rewrites 也一起整理了,想說既然都整理了,不如就寫篇文章好了。
先聲明一下,雖然標題寫「請求的生命週期」,但其實沒有講得很底層,比較像是從實務角度去理解 Next.js 處理請求的幾個機制。只是我沒想到比這個更好的標題 ╮(╯_╰)╭
1. 先釐清名詞
在 Next.js 裡面有三個東西很容易搞混:
- Rewrites:在
next.config.ts裡面設定的路由轉發規則 - Middleware:放在專案根目錄的
middleware.ts,每個 request 都會經過的程式碼(Next.js 15 以前) - Proxy:Next.js 16 把 Middleware 改名了,檔案從
middleware.ts變成proxy.ts
簡單說 Rewrites 是設定檔層級的東西,Middleware/Proxy 是程式碼層級的東西,它們做的事不太一樣。
2. Rewrites:設定檔裡的路由轉發
Rewrites 的概念很直覺,就是「把 A 路徑偷偷導到 B 位址」。最常見的場景就是前端要串後端 API 的時候:
import { NextConfig } from 'next'
const nextConfig: NextConfig = {
async rewrites() {
return [
{
source: '/api/:path*',
destination: 'https://your-backend.com/:path*',
},
]
},
}
export default nextConfig前端打 /api/users,Next.js 幫你轉到 https://your-backend.com/users,瀏覽器完全不知道這件事,所以也不會有 CORS 的問題。
條件式的 Rewrites
比較少人知道的是,Rewrites 可以根據 Header、Cookie、Query 來決定要不要觸發:
module.exports = {
async rewrites() {
return [
{
source: '/:path*',
has: [
{
type: 'header',
key: 'x-rewrite-me',
},
],
destination: '/another-page',
},
]
},
}看起來好像很彈性?但其實能做的事還是有限,就是比對條件然後決定要不要轉發,沒辦法執行任何邏輯。
Fallback 模式
Rewrites 還有一個 fallback 模式,在漸進式遷移的時候特別好用:
module.exports = {
async rewrites() {
return {
fallback: [
{
source: '/:path*',
destination: 'https://old-website.com/:path*',
},
],
}
},
}所有 Next.js 的路由都比對完了還是沒找到的話,就丟回舊網站。這樣可以一頁一頁慢慢搬,不用一次全部重寫,誰受得了一次全部重寫 (´;ω;`)
3. Middleware:請求進來的第一道關卡
Middleware 就不一樣了,它是一段真正會執行的程式碼。放在專案根目錄的 middleware.ts,每個符合條件的 request 都會先經過它。
最經典的用法就是 Auth 驗證:
import { NextResponse } from 'next/server'
import type { NextRequest } from 'next/server'
import { decrypt } from '@/app/lib/session'
import { cookies } from 'next/headers'
const protectedRoutes = ['/dashboard']
const publicRoutes = ['/login', '/signup', '/']
export default async function middleware(req: NextRequest) {
const path = req.nextUrl.pathname
const isProtectedRoute = protectedRoutes.includes(path)
const isPublicRoute = publicRoutes.includes(path)
const cookie = (await cookies()).get('session')?.value
const session = await decrypt(cookie)
// 沒登入想進 dashboard?請回去
if (isProtectedRoute && !session?.userId) {
return NextResponse.redirect(new URL('/login', req.nextUrl))
}
// 已經登入還跑去 login 頁?幫你導過去
if (isPublicRoute && session?.userId &&
!req.nextUrl.pathname.startsWith('/dashboard')) {
return NextResponse.redirect(new URL('/dashboard', req.nextUrl))
}
return NextResponse.next()
}
export const config = {
matcher: ['/((?!api|_next/static|_next/image|.*\\.png$).*)'],
}這裡有幾個重點:
NextResponse.redirect()可以把使用者導去別的頁面NextResponse.next()代表放行,讓 request 繼續往下走config.matcher決定哪些路徑要經過 Middleware,上面那串 regex 的意思是「除了 API route、靜態檔案、圖片以外的所有路徑」
除了 redirect,還能幹嘛
Middleware 能做的事比 Rewrites 多很多:
import { NextResponse } from 'next/server'
import type { NextRequest } from 'next/server'
export function middleware(request: NextRequest) {
// 改 request header
const requestHeaders = new Headers(request.headers)
requestHeaders.set('x-request-id', crypto.randomUUID())
const response = NextResponse.next({
request: { headers: requestHeaders },
})
// 改 response header
response.headers.set('x-response-time', Date.now().toString())
return response
}讀寫 Header、讀寫 Cookie、動態 Rewrite、直接回 Response... 基本上在 request 層級能做的事它都能做。
4. Next.js 16 的改名:Middleware → Proxy
這大概是升級 Next.js 16 最容易踩到的坑之一。
| Next.js 15 以前 | Next.js 16+ | |
|---|---|---|
| 檔案名稱 | middleware.ts | proxy.ts |
| 函式名稱 | middleware() | proxy() |
| Runtime | Edge Runtime | Node.js Runtime |
| 設定名稱 | skipMiddlewareUrlNormalize | skipProxyUrlNormalize |
改名這件事我覺得可以理解,因為它本質上就是一個站在 client 跟 server 之間的代理人,叫 Proxy 確實比 Middleware 更精準。
但比較大的改變是 Edge Runtime 不再支援了。以前 Middleware 跑在 Edge 上,有些 Node.js 的 API 不能用。現在改成跑在 Node.js Runtime,反而限制更少了,不過如果你之前有依賴 Edge Runtime 的特性,就要注意一下。
升級的話官方有提供 codemod:
npx @next/codemod@latest upgrade它會幫你把 middleware.ts 改名成 proxy.ts,函式名稱跟相關的設定也會一起更新。我自己跑過一次,基本上沒什麼問題 (´・ω・`)
5. 一個請求到底走過什麼路
把上面講的東西串起來,一個 request 進到 Next.js 之後大概是這樣走的:
Client 發出 Request
↓
Proxy / Middleware
(程式碼層:auth 驗證、加 header、redirect)
↓
Rewrites
(設定檔層:路徑轉發)
↓
Next.js Page / API Route
(實際處理請求的地方)
↓
Response 回到 Client
Proxy/Middleware 先跑,Rewrites 後跑。所以你可以在 Proxy 裡面先驗證身分、加上 x-user-id 之類的 header,然後 Rewrites 再把 /api/* 轉發到後端服務,後端就可以直接從 header 拿到使用者資訊。
兩個各做各的事,不衝突。
6. 所以到底什麼時候用哪個
寫到這裡其實蠻明顯的了:
- 如果你的需求是「把路徑 A 對應到路徑 B」,用 Rewrites 就夠了,不用寫程式碼
- 如果你需要「根據條件做判斷、改 header、改 cookie」,用 Middleware/Proxy
- 兩個可以同時用,它們本來就是設計來搭配的
以後有人問你 Next.js 的 Proxy 跟 Rewrites 差在哪的時候,可以跟他說:一個是設定檔層級的路由轉發,一個是程式碼層級的請求攔截,就這樣 ヽ(✿゚▽゚)ノ