Skip to content

What Actually Belongs in Next.js Middleware?

Six Experts Debate Best Practices and Pitfalls

Next.js Middleware 到底該放什麼?

六位專家圓桌激辯最佳實踐與禁忌

"Middleware runs before cached content and route matching. That power comes with sharp edges." — Next.js Documentation

「Middleware 在快取內容和路由匹配之前運行。這種能力伴隨著尖銳的邊角。」—— Next.js 文件


Context: Why This Debate Matters Now

Next.js Middleware has gone through a turbulent four years. Introduced in v12 (2021) as an Edge-only interception layer, it has since survived a critical security vulnerability (CVE-2025-29927), gained Node.js runtime support (v15.5), and been renamed from middleware.ts to proxy.ts in v16 — a philosophical signal that the Next.js team wants developers to use it less, not more.

Yet middleware remains the only mechanism that runs before the cache layer, before route matching, before any rendering. For certain problems — authentication gates, A/B testing, geo-routing — there is no alternative. The question is not whether to use middleware, but what belongs there and what doesn't.

We assembled six experts with sharply different perspectives to hash this out.

背景:為什麼這場辯論此刻至關重要

Next.js Middleware 經歷了動盪的四年。它在 v12(2021)作為 Edge-only 攔截層引入,此後經歷了一個嚴重安全漏洞(CVE-2025-29927),獲得了 Node.js runtime 支援(v15.5),並在 v16 中從 middleware.ts 重命名為 proxy.ts——這是一個哲學信號,表明 Next.js 團隊希望開發者更少而非更多地使用它。

然而 middleware 仍然是唯一在快取層之前、路由匹配之前、任何渲染之前運行的機制。對於某些問題——認證閘門、A/B 測試、地理路由——沒有替代方案。問題不是要不要用 middleware,而是什麼該放進去、什麼不該

我們召集了六位觀點截然不同的專家來徹底辯論這件事。


Roundtable Participants

  • Lina Torres — Staff Engineer at a fintech startup. Burned by CVE-2025-29927 on self-hosted Next.js. Advocates defense-in-depth: middleware is the first gate, never the only gate.
  • Kevin Wu — Performance engineer, ex-Vercel. Obsessed with TTFB. Believes middleware should do almost nothing — every millisecond there is paid on every request.
  • Priya Sharma — Full-stack lead at a SaaS company running multi-tenant architecture. Believes middleware is the backbone of multi-tenant routing and should be embraced, not feared.
  • Ryan O'Brien — Open source maintainer of two popular Next.js auth libraries. Thinks the single-file limitation is the root of all evil and middleware composition patterns are a necessary hack.
  • Mei Zhang — Frontend architect at an e-commerce company. Runs A/B tests and feature flags in middleware across 50M monthly visitors. Believes middleware is the only correct place for pre-render decisions.
  • Tomás García — DevRel engineer and educator. Focuses on what beginners get wrong and argues most middleware tutorials on the internet are harmful.

圓桌會議參與者

  • Lina Torres — 金融科技新創 Staff Engineer。在自架 Next.js 上被 CVE-2025-29927 燒傷過。主張縱深防禦:middleware 是第一道門,絕不是唯一一道門。
  • Kevin Wu — 效能工程師,前 Vercel 員工。對 TTFB 極度執著。相信 middleware 應該幾乎什麼都不做——那裡的每一毫秒都要為每個請求付出代價。
  • Priya Sharma — SaaS 公司全端 lead,運行多租戶架構。相信 middleware 是多租戶路由的骨幹,應該擁抱而非恐懼。
  • Ryan O'Brien — 兩個熱門 Next.js 認證庫的開源維護者。認為單一檔案限制是萬惡之源,middleware 組合模式是不得已的 hack。
  • Mei Zhang — 電商公司前端架構師。在 middleware 中對 5000 萬月訪客執行 A/B 測試和 feature flags。相信 middleware 是預渲染決策唯一正確的位置
  • Tomás García — DevRel 工程師兼教育者。關注初學者最常犯的錯誤,認為網路上大多數 middleware 教學是有害的。

Round 1: What Belongs in Middleware?

Moderator: Let's start with the fundamental question. What should middleware actually do?

Kevin: I'll make this simple. Middleware should do three things: redirect, rewrite, and set headers. That's it. If your middleware function is longer than 20 lines, you've already gone too far. Middleware runs before the cache layer — every single request hits it, including prefetches from <Link> components. Every millisecond you add there is multiplied by your entire traffic volume.

Priya: That's absurdly reductive. Middleware is the only place to do subdomain-based tenant resolution before the page renders. I rewrite acme.example.com/dashboard to /acme/dashboard in middleware. Where else would I do that? next.config.js rewrites can't read the Host header dynamically.

Kevin: I didn't say rewrites are wrong. I said your middleware should be thin. Tenant resolution is a rewrite — perfectly fine. But I've seen teams put database queries in middleware to look up tenant configuration. That's insane. Read the hostname, rewrite the path, get out. Look up the config in a Server Component.

Mei: Let me add A/B testing. Middleware is the canonical place for variant assignment because you need the decision before rendering. You read a cookie, assign a bucket if missing, set the cookie, rewrite to the variant path. If you do this in a Server Component, you get layout shift. If you do it in client-side JavaScript, you get a flash of wrong content. Middleware is the only zero-flicker option.

Tomás: And this is exactly where beginners go wrong. They read Mei's use case and think "great, I'll put my feature flag evaluation in middleware" — then they add a fetch() to LaunchDarkly on every request, adding 100ms of latency to every page load. Mei, you're not fetching flags in middleware, are you?

Mei: Absolutely not. I read flags from a cookie or a KV store. The actual flag evaluation happens in a background job that writes to Upstash KV. Middleware only reads the result. The total overhead is under 5ms.

Tomás: Exactly. But the tutorials don't say that. They show await fetch('https://flags-api.com') in middleware and call it "best practice."

第一回合:什麼該放進 Middleware?

主持人: 從根本問題開始。Middleware 實際上應該做什麼?

Kevin: 我說簡單點。Middleware 應該做三件事:redirect、rewrite 和設定 headers。就這樣。如果你的 middleware 函式超過 20 行,你已經走太遠了。Middleware 在快取層之前運行——每一個請求都會觸發它,包括 <Link> 元件的 prefetch。你在那裡增加的每一毫秒都會乘以你的整個流量。

Priya: 那太極端簡化了。Middleware 是在頁面渲染之前進行基於子域名的租戶解析的唯一位置。我在 middleware 中把 acme.example.com/dashboard 改寫為 /acme/dashboard。我還能在哪裡做?next.config.js 的 rewrites 無法動態讀取 Host 標頭。

Kevin: 我沒說 rewrite 是錯的。我說你的 middleware 應該是薄的。租戶解析就是一個 rewrite——完全沒問題。但我見過團隊在 middleware 裡放資料庫查詢來查找租戶配置。那是瘋了。讀取 hostname,rewrite 路徑,閃人。在 Server Component 裡查找配置。

Mei: 讓我加上 A/B 測試。Middleware 是變體分配的標準位置,因為你需要在渲染之前做出決策。你讀取 cookie,如果缺失就分配 bucket,設定 cookie,rewrite 到變體路徑。如果在 Server Component 裡做,你會得到版面偏移。如果在客戶端 JavaScript 裡做,你會得到錯誤內容的閃爍。Middleware 是唯一零閃爍的選項。

Tomás: 而這正是初學者出錯的地方。他們讀了 Mei 的用例然後想「太好了,我來把 feature flag 評估放到 middleware」——然後他們在每個請求上加了一個 fetch() 到 LaunchDarkly,給每次頁面載入增加 100ms 延遲。Mei,你沒有在 middleware 裡 fetch flags 吧?

Mei: 絕對沒有。 我從 cookie 或 KV store 讀取 flags。實際的 flag 評估發生在一個背景任務中,寫入 Upstash KV。Middleware 只讀取結果。總開銷低於 5ms。

Tomás: 正是如此。但教學裡不會這麼說。它們在 middleware 裡展示 await fetch('https://flags-api.com') 然後稱之為「最佳實踐」。


Vote: The Core Legitimate Use Cases

Use CaseLegitimate?Votes (Y/N)
Redirects & rewritesYes6-0
Header manipulationYes6-0
A/B test bucketing (cookie-based, no fetch)Yes6-0
Multi-tenant subdomain routingYes6-0
i18n locale detection & routingYes5-1 (Kevin abstains: "use next.config.js i18n if possible")
CSP / security headersYes6-0
Bot detection (User-Agent check)Yes5-1 (Tomás: "fragile, but acceptable")
Rate limiting with edge KVConditional4-2 (Kevin & Tomás: "adds latency to every request")
Auth token verificationConditional3-3 (see Round 2)
Database queriesNo0-6
Feature flag fetch to external APINo0-6
Complex business logicNo0-6

Result: Consensus on the "thin gateway" principle — middleware should make routing decisions based on data already present in the request (cookies, headers, URL) or available in sub-5ms reads (edge KV). Any external fetch is a red flag.

投票:核心合法用例

用例合法?票數(同意/反對)
重導向與改寫6-0
標頭操作6-0
A/B 測試分桶(基於 cookie,無 fetch)6-0
多租戶子域名路由6-0
i18n 語言偵測與路由5-1(Kevin 棄權:「盡可能用 next.config.js i18n」)
CSP / 安全標頭6-0
Bot 偵測(User-Agent 檢查)5-1(Tomás:「脆弱,但可接受」)
使用 edge KV 的速率限制有條件4-2(Kevin 和 Tomás:「為每個請求增加延遲」)
認證 token 驗證有條件3-3(見第二回合)
資料庫查詢0-6
對外部 API 的 feature flag fetch0-6
複雜業務邏輯0-6

結果:對「薄閘道」原則達成共識——middleware 應該基於請求中已有的資料(cookies、headers、URL)或可在 5ms 以內讀取的資料(edge KV)來做路由決策。任何外部 fetch 都是紅旗。


Round 2: The Authentication War

Moderator: Authentication in middleware is clearly the most divisive topic. Let's dig into it.

Lina: I'll tell you exactly why this is divisive. In March 2025, CVE-2025-29927 let attackers bypass all middleware by sending a crafted x-middleware-subrequest header. Every Next.js app that relied solely on middleware for auth was wide open. Authentication, CSP headers, rate limiting — all bypassed. We were self-hosting and had no Vercel routing layer to save us. We discovered the breach two days after disclosure.

Ryan: What happened to your app?

Lina: We got lucky. Our database access layer had its own auth checks — that's what actually stopped unauthorized data access. But our admin dashboard was temporarily accessible because it only checked auth in middleware. After that, I became a zealot for defense-in-depth. Middleware is a speed bump, not a wall.

Ryan: I maintain two auth libraries and I fully agree with Lina. Here's the pattern every auth library now recommends:

Layer 1: Middleware — fast token presence check, redirect if missing
Layer 2: Server Component / Route Handler — full token verification
Layer 3: Data Access Layer — verify permissions before every database query

If any single layer fails, the others catch it. After CVE-2025-29927, Vercel explicitly recommends this in their postmortem.

Priya: So what should middleware actually do for auth?

Ryan: Check if a session cookie exists. That's it. Don't verify the JWT signature in middleware — do that in the Server Component. Middleware's job is to redirect unauthenticated users to /login as fast as possible. The actual cryptographic verification happens deeper in the stack.

Kevin: Interrupts. Wait. I disagree. If you're using jose for JWT verification, it's edge-compatible and adds maybe 2-3ms. That's worth it because it prevents the entire page from rendering for an invalid token. Why waste server resources rendering a dashboard page only to have the Server Component reject the token?

Lina: Because if middleware gets bypassed again, you've rendered the page for an attacker. That's Kevin's point reversed.

Kevin: CVE-2025-29927 was patched. You're designing for a vulnerability that no longer exists.

Lina: I'm designing for the next vulnerability that doesn't exist yet. That's what defense-in-depth means.

Tomás: Can I offer the beginner perspective? Most tutorials show jose JWT verification in middleware as the auth pattern. Beginners copy it and think they're done. They don't add a data access layer. They don't verify in Server Components. The middleware tutorial is their entire security architecture. That's the real danger — not the pattern itself, but that people treat it as complete.

第二回合:認證之戰

主持人: 在 middleware 中做認證顯然是最具爭議的話題。讓我們深入探討。

Lina: 我來告訴你們為什麼這麼有爭議。2025 年 3 月,CVE-2025-29927 讓攻擊者可以透過發送一個特製的 x-middleware-subrequest 標頭來繞過所有 middleware。每個僅依賴 middleware 做認證的 Next.js 應用都門戶大開。認證、CSP 標頭、速率限制——全部被繞過。我們是自架的,沒有 Vercel 路由層來拯救我們。我們在漏洞披露兩天後才發現入侵。

Ryan: 你的應用怎麼了?

Lina: 我們很幸運。我們的資料庫存取層有自己的認證檢查——那才是真正阻止未授權資料存取的東西。但我們的管理後台暫時可以被存取,因為它只在 middleware 裡檢查認證。從那之後,我成了縱深防禦的狂熱者。Middleware 是減速帶,不是牆。

Ryan: 我維護兩個認證庫,我完全同意 Lina。這是現在每個認證庫都推薦的模式:

第一層:Middleware——快速 token 存在性檢查,缺失則重導向
第二層:Server Component / Route Handler——完整 token 驗證
第三層:Data Access Layer——在每次資料庫查詢前驗證權限

如果任何單一層失敗,其他層會接住。CVE-2025-29927 之後,Vercel 在事後分析中明確推薦這個模式。

Priya: 所以 middleware 對認證實際應該做什麼?

Ryan: 檢查 session cookie 是否存在。就這樣。不要在 middleware 裡驗證 JWT 簽名——在 Server Component 裡做。Middleware 的工作是盡快把未認證用戶重導向到 /login。實際的密碼學驗證發生在更深的堆疊中。

Kevin: 打斷。 等等,我不同意。如果你用 jose 做 JWT 驗證,它是 edge 相容的,大概只增加 2-3ms。這是值得的,因為它阻止了整個頁面為無效 token 渲染。為什麼浪費伺服器資源渲染一個 dashboard 頁面,結果 Server Component 又拒絕了 token?

Lina: 因為如果 middleware 又被繞過了,你就為攻擊者渲染了頁面。那是 Kevin 的論點反過來。

Kevin: CVE-2025-29927 已經修補了。你在為一個不再存在的漏洞設計。

Lina: 我在為下一個還不存在的漏洞設計。這就是縱深防禦的意義。

Tomás: 我能提供初學者視角嗎?大多數教學展示 jose JWT 驗證在 middleware 中作為唯一的認證模式。初學者複製它然後以為完成了。他們不加 data access layer。他們不在 Server Components 中驗證。Middleware 教學就是他們的整個安全架構。這才是真正的危險——不是模式本身,而是人們把它當作完整方案。


Vote: Should JWT Verification Live in Middleware?

VoterPositionReasoning
LinaNo — cookie existence check only"CVE-2025-29927 taught me: never trust a single layer"
KevinYes — lightweight jose verification"2-3ms to avoid rendering entire pages for invalid tokens"
PriyaYes, but with data layer backup"For multi-tenant apps, I need to verify the tenant claim in middleware"
RyanNo — presence check in middleware, verify in Server Component"As a library maintainer, I've seen too many apps without backup layers"
MeiNo opinion — "I don't do auth in middleware"
TomásNo — "beginners will skip the backup layer""The pattern is fine for experts; dangerous as a tutorial"

Result: 3-3 tie. The panel acknowledges this is genuinely situation-dependent. Consensus compromise: if you verify JWT in middleware, you MUST also verify at the data access layer. Middleware verification is an optimization, never the sole check.

投票:JWT 驗證應該放在 Middleware 嗎?

投票者立場理由
Lina不應該——只做 cookie 存在性檢查「CVE-2025-29927 教會我:永遠不要信任單一層」
Kevin應該——輕量的 jose 驗證「用 2-3ms 避免為無效 token 渲染整個頁面」
Priya應該,但需要 data layer 備份「在多租戶應用中,我需要在 middleware 驗證租戶聲明」
Ryan不應該——middleware 做存在性檢查,Server Component 做驗證「作為庫維護者,我見過太多沒有備份層的應用」
Mei沒意見——「我不在 middleware 做認證」
Tomás不應該——「初學者會跳過備份層」「這個模式對專家沒問題;作為教學很危險」

結果:3-3 平手。小組承認這確實取決於情境。共識妥協:如果你在 middleware 驗證 JWT,你必須也在 data access layer 驗證。Middleware 驗證是一種優化,永遠不是唯一的檢查。


Round 3: The Performance Razor

Moderator: Kevin, you keep mentioning performance. Give us the numbers.

Kevin: Here's the reality nobody wants to hear. Middleware runs before the cache. The execution order is:

Request → Middleware → Cache Check → Route Match → Render

Not this:

Request → Cache Check → Middleware → Render  (WRONG)

That means even for a fully static ISR page that could be served from CDN cache in 20ms, the request still passes through middleware first. If your middleware adds 80ms, your 20ms cached page now takes 100ms. You've 5x'd your TTFB for every cached page in your entire application.

Priya: That's a worst case. What's realistic?

Kevin: Edge warm: 37-60ms TTFB. Edge cold start: 60-250ms. A well-written middleware with just cookie reads and a rewrite adds 3-8ms. A middleware with one fetch() call adds 50-150ms. A middleware with two fetch() calls adds 100-300ms. You do the math.

Mei: That's why the matcher config is so critical. My middleware only runs on routes that need A/B testing — about 15% of our routes. The other 85% skip middleware entirely because of the matcher.

Kevin: Exactly. And this is the second thing beginners get wrong after auth. They write:

ts
export function middleware(request: NextRequest) {
  if (!request.nextUrl.pathname.startsWith('/dashboard')) {
    return NextResponse.next();
  }
  // ...logic
}

Instead of:

ts
export const config = {
  matcher: ['/dashboard/:path*'],
}

The first version invokes the middleware function for every request and returns early. The second version lets the framework skip invocation entirely. Same logic, dramatically different performance characteristics.

Tomás: And there's a hidden gotcha. Even with a matcher that excludes _next/static and _next/image, middleware will still be invoked for _next/data requests during client-side navigation. That's documented but not well-known. So your middleware runs more often than you think.

Kevin: The canonical matcher pattern that most people should start with:

ts
export const config = {
  matcher: ['/((?!api|_next/static|_next/image|favicon.ico|sitemap.xml|robots.txt).*)'],
}

And then narrow it further based on your actual needs.

第三回合:效能利刃

主持人: Kevin,你一直提到效能。給我們數字。

Kevin: 這是沒人想聽的現實。Middleware 在快取之前運行。執行順序是:

請求 → Middleware → 快取檢查 → 路由匹配 → 渲染

不是這個:

請求 → 快取檢查 → Middleware → 渲染(錯誤)

這意味著即使是一個完全靜態的 ISR 頁面,本來可以從 CDN 快取在 20ms 內提供,請求仍然會先經過 middleware。如果你的 middleware 增加 80ms,你的 20ms 快取頁面現在需要 100ms。你把整個應用中每個快取頁面的 TTFB 變成了 5 倍

Priya: 那是最壞情況。實際情況呢?

Kevin: Edge 溫熱:37-60ms TTFB。Edge 冷啟動:60-250ms。一個寫得好的 middleware 只做 cookie 讀取和 rewrite 增加 3-8ms。一個帶一次 fetch() 呼叫的 middleware 增加 50-150ms。帶兩次 fetch() 呼叫的增加 100-300ms。你自己算。

Mei: 這就是為什麼 matcher config 如此關鍵。我的 middleware 只在需要 A/B 測試的路由上運行——大約是我們路由的 15%。其他 85% 因為 matcher 完全跳過 middleware。

Kevin: 正是如此。這是初學者在認證之後犯的第二個錯誤。他們寫:

ts
export function middleware(request: NextRequest) {
  if (!request.nextUrl.pathname.startsWith('/dashboard')) {
    return NextResponse.next();
  }
  // ...邏輯
}

而不是:

ts
export const config = {
  matcher: ['/dashboard/:path*'],
}

第一個版本為每個請求調用 middleware 函式然後提前返回。第二個版本讓框架完全跳過調用。相同邏輯,效能特徵天差地別。

Tomás: 還有一個隱藏陷阱。即使用了排除 _next/static_next/image 的 matcher,middleware 仍然會在客戶端導航時為 _next/data 請求被調用。這有文件記載但不太為人所知。所以你的 middleware 運行得比你以為的更頻繁。

Kevin: 大多數人應該從這個經典 matcher 模式開始:

ts
export const config = {
  matcher: ['/((?!api|_next/static|_next/image|favicon.ico|sitemap.xml|robots.txt).*)'],
}

然後根據你的實際需求進一步收窄。


Vote: Should Middleware Contain Any async/await?

VoterPositionReasoning
KevinAvoid unless absolutely necessary"Every async call is 50-150ms on your critical path"
MeiYes, for edge KV reads only"Sub-5ms reads from Upstash KV are acceptable"
PriyaYes, for tenant resolution"Some rewrites need a quick KV lookup"
LinaMinimal"Cookie reads and sync checks only"
RyanAvoid"jose JWT verify is the exception, not the rule"
TomásAvoid in tutorials"Teach sync-only middleware first"

Result: 4-2 in favor of avoiding async. Exception: sub-5ms edge KV reads are acceptable when the use case demands it.

投票:Middleware 應該包含任何 async/await 嗎?

投票者立場理由
Kevin除非絕對必要否則避免「每次 async 呼叫在你的關鍵路徑上就是 50-150ms」
Mei可以,僅限 edge KV 讀取「從 Upstash KV 讀取低於 5ms 是可接受的」
Priya可以,用於租戶解析「某些 rewrite 需要快速的 KV 查找」
Lina最少化「只做 cookie 讀取和同步檢查」
Ryan避免jose JWT 驗證是例外,不是規則」
Tomás在教學中避免「先教只有同步的 middleware」

結果:4-2 傾向避免 async。例外:當用例要求時,低於 5ms 的 edge KV 讀取是可接受的。


Round 4: The Single-File Problem and Composition

Moderator: Ryan, you mentioned the single-file limitation. Expand on that.

Ryan: Next.js supports exactly one middleware.ts file — or proxy.ts in v16. You can't have app/dashboard/middleware.ts and app/api/middleware.ts. Everything goes in one file. For a small app, this is fine. For a large app with auth, i18n, A/B testing, rate limiting, and multi-tenant routing all in middleware? You end up with a 300-line monster.

Priya: We have 280 lines. I'm not proud of it.

Ryan: The community workaround is a chain pattern:

ts
type MiddlewareFn = (req: NextRequest) => NextResponse | undefined;

function chain(fns: MiddlewareFn[], req: NextRequest): NextResponse {
  for (const fn of fns) {
    const result = fn(req);
    if (result && result.status !== 200) return result;
  }
  return NextResponse.next();
}

export function middleware(req: NextRequest) {
  return chain([withRateLimit, withAuth, withI18n, withTenant], req);
}

It works, but it's a hack. Headers and cookies set in one middleware function aren't visible to the next one in the chain unless you explicitly pass them. The NextResponse API wasn't designed for composition.

Lina: The Next.js team knows this. They've discussed "Request Interceptors" — a nested, file-system-based middleware API. But there's no timeline.

Kevin: Honestly? I think the single-file limitation is a feature. It forces you to keep middleware small. If you had per-route middleware files, teams would put full business logic in app/dashboard/middleware.ts and create untraceable request processing pipelines. The constraint is the design.

Ryan: Shakes head. That's like saying JavaScript shouldn't have modules because people might make too many files. The constraint isn't the design — it's a limitation they haven't resolved yet.

Priya: The v16 rename to proxy.ts actually supports Kevin's view. The team is saying: "This is a proxy layer. It's supposed to be thin." The name change discourages overuse.

Ryan: And yet they give us the full NextResponse API, cookie manipulation, header injection, waitUntil()... If they wanted a thin proxy, they'd give us a thin API. The API says "do things here." The naming says "don't do things here." That's mixed messaging.

第四回合:單一檔案問題與組合

主持人: Ryan,你提到了單一檔案限制。展開說說。

Ryan: Next.js 只支援一個 middleware.ts 檔案——或 v16 的 proxy.ts。你不能有 app/dashboard/middleware.tsapp/api/middleware.ts。所有東西都放在一個檔案裡。對小應用來說沒問題。但對一個在 middleware 裡有認證、i18n、A/B 測試、速率限制和多租戶路由的大應用?你最終得到一個 300 行的怪物。

Priya: 我們有 280 行。我並不自豪。

Ryan: 社群的解決方案是一個 chain 模式:

ts
type MiddlewareFn = (req: NextRequest) => NextResponse | undefined;

function chain(fns: MiddlewareFn[], req: NextRequest): NextResponse {
  for (const fn of fns) {
    const result = fn(req);
    if (result && result.status !== 200) return result;
  }
  return NextResponse.next();
}

export function middleware(req: NextRequest) {
  return chain([withRateLimit, withAuth, withI18n, withTenant], req);
}

它可以用,但它是 hack。在一個 middleware 函式中設定的 headers 和 cookies 不會對 chain 中的下一個函式可見,除非你顯式傳遞它們。NextResponse API 不是為組合設計的。

Lina: Next.js 團隊知道這個問題。他們討論過「Request Interceptors」——一個嵌套的、基於檔案系統的 middleware API。但沒有時間表。

Kevin: 老實說?我認為單一檔案限制是一個特性。它強迫你保持 middleware 小。如果你有按路由的 middleware 檔案,團隊會把完整的業務邏輯放到 app/dashboard/middleware.ts 裡,然後造出無法追蹤的請求處理管線。限制就是設計。

Ryan: 搖頭。 那就像說 JavaScript 不應該有模組因為人們可能會建太多檔案。限制不是設計——是他們尚未解決的局限。

Priya: v16 重命名為 proxy.ts 實際上支持 Kevin 的觀點。團隊在說:「這是一個代理層。它應該是薄的。」名稱變更阻止了過度使用。

Ryan: 然而他們給了我們完整的 NextResponse API、cookie 操作、header 注入、waitUntil()⋯⋯ 如果他們想要一個薄代理,他們會給我們一個薄 API。API 說「在這裡做事」。命名說「不要在這裡做事」。這是混合訊息。


Vote: Is the Single-File Limitation a Design Choice or a Flaw?

VoterPosition
KevinDesign choice — forces discipline
PriyaFlaw — my 280-line file proves it
RyanFlaw — composition should be first-class
LinaFlaw — but I understand the reasoning
MeiDesign choice — works fine with narrow matchers
TomásFlaw — bad for teaching separation of concerns

Result: 4-2 — majority considers it a flaw, but acknowledges the discipline argument.

投票:單一檔案限制是設計選擇還是缺陷?

投票者立場
Kevin設計選擇——強制紀律
Priya缺陷——我的 280 行檔案證明了這點
Ryan缺陷——組合應該是一等公民
Lina缺陷——但我理解背後的理由
Mei設計選擇——配合窄 matcher 用起來很好
Tomás缺陷——不利於教學關注點分離

結果:4-2——多數認為是缺陷,但承認紀律論點有道理。


Round 5: The middleware.ts to proxy.ts Shift

Moderator: Next.js 16 renamed middleware.ts to proxy.ts and defaulted to Node.js runtime. What does this mean practically?

Kevin: Three huge changes. First, naming clarity. "Middleware" made people think of Express middleware — a place to do arbitrary server-side processing. "Proxy" communicates that it operates at the network boundary. You don't put business logic in a proxy.

Priya: Second, Node.js runtime by default. This is massive. For four years, the Edge Runtime restriction meant we couldn't use jsonwebtoken, bcrypt, database drivers, Firebase Admin SDK, or any npm package that touches Node.js APIs. That's gone. proxy.ts defaults to Node.js. You can import anything.

Lina: Which is a double-edged sword. Now people can put database queries in proxy, and they will. The Edge Runtime was an accidental guardrail against overuse. With Node.js runtime, there's nothing stopping someone from importing Prisma into proxy.ts.

Kevin: Leans forward. And that would be a disaster for performance. Edge Runtime had a 1MB bundle limit and restricted APIs, which forced lightweight code. Node.js runtime has no such constraints. I predict we'll see proxy files that import half the application's dependencies and add 500ms to every request.

Tomás: The v16 migration is automated: npx @next/codemod@canary middleware-to-proxy .. It renames the file and updates the exported function name. But it doesn't change your code's architecture. If your middleware was already too heavy, your proxy will be too heavy too.

Ryan: The interesting thing is what the Next.js team said in the v16 blog post: "We recommend users avoid relying on Middleware unless no other options exist." They're telling us to use it less. Server Actions, Route Handlers, next.config.js redirects, and Cache Components are all meant to reduce the cases where you need a proxy at all.

Mei: For my A/B testing use case, proxy.ts with Node.js runtime changes nothing. I'm still reading cookies and rewriting paths. But I appreciate that I could now fall back to jsonwebtoken instead of jose if I wanted to. More options is better.

第五回合:從 middleware.ts 到 proxy.ts 的轉變

主持人: Next.js 16 把 middleware.ts 重命名為 proxy.ts 並預設使用 Node.js runtime。這實際上意味著什麼?

Kevin: 三個巨大變化。第一,命名清晰度。「Middleware」讓人聯想到 Express middleware——一個做任意伺服器端處理的地方。「Proxy」傳達了它在網路邊界運作的訊息。你不會把業務邏輯放在 proxy 裡。

Priya: 第二,預設 Node.js runtime。這是巨大的。四年來,Edge Runtime 限制意味著我們不能用 jsonwebtokenbcrypt、資料庫驅動、Firebase Admin SDK,或任何觸碰 Node.js API 的 npm 套件。這個限制消失了。proxy.ts 預設 Node.js。你可以 import 任何東西。

Lina: 這是一把雙刃劍。現在人們可以在 proxy 裡放資料庫查詢,而且他們這麼做。Edge Runtime 是一個意外的防護欄,防止過度使用。有了 Node.js runtime,沒有什麼能阻止人把 Prisma import 到 proxy.ts

Kevin: 身體前傾。 那對效能會是災難。Edge Runtime 有 1MB bundle 限制和受限 API,迫使程式碼保持輕量。Node.js runtime 沒有這些限制。我預測我們會看到 import 了應用一半依賴、為每個請求增加 500ms 的 proxy 檔案。

Tomás: v16 遷移是自動化的:npx @next/codemod@canary middleware-to-proxy .。它重命名檔案並更新匯出的函式名稱。但它不會改變你程式碼的架構。如果你的 middleware 已經太重了,你的 proxy 也會太重。

Ryan: 有趣的是 Next.js 團隊在 v16 部落格文章中說的:「我們建議用戶除非沒有其他選擇,否則避免依賴 Middleware。」 他們在告訴我們少用。Server Actions、Route Handlers、next.config.js redirects 和 Cache Components 都旨在減少你需要 proxy 的場景。

Mei: 對於我的 A/B 測試用例,帶 Node.js runtime 的 proxy.ts 什麼都不改變。我仍然在讀 cookies 和 rewrite 路徑。但我很感激現在如果需要可以退回到 jsonwebtoken 而不是只能用 jose。更多選項更好。


Vote: Will proxy.ts with Node.js Runtime Lead to More Misuse?

VoterPosition
KevinYes — removing the guardrails will invite abuse
LinaYes — people will import Prisma into proxy.ts
PriyaNo — the naming shift discourages it
RyanYes — library authors will ship heavy middleware plugins
MeiNo — experienced teams know better
TomásYes — tutorials will get worse before they get better

Result: 4-2 predict more misuse. The removal of Edge Runtime constraints, while solving real problems, removes the accidental discipline that kept middleware lightweight.

投票:帶 Node.js Runtime 的 proxy.ts 會導致更多誤用嗎?

投票者立場
Kevin——移除防護欄會招來濫用
Lina——人們會把 Prisma import 到 proxy.ts
Priya不會——命名轉變會阻止
Ryan——庫作者會推出重量級 middleware 外掛
Mei不會——有經驗的團隊知道該怎麼做
Tomás——教學會在變好之前先變差

結果:4-2 預測會有更多誤用。Edge Runtime 限制的移除雖然解決了真正的問題,但也移除了讓 middleware 保持輕量的意外紀律。


Final Verdict: The Middleware Best Practices Checklist

After five rounds of heated debate, the panel converged on a practical checklist that all six experts endorse.

The Golden Rules

1. Middleware is a thin gateway, not an application layer. Redirect, rewrite, set headers. If your logic doesn't fit in 30 lines, it probably doesn't belong in middleware.

2. Never trust middleware as your sole security boundary. After CVE-2025-29927: three-layer defense — middleware → Server Component → Data Access Layer. No exceptions.

3. Use matcher config, not conditional logic. Static matchers let the framework skip invocation entirely. Always exclude static assets. Always exclude your redirect targets.

ts
export const config = {
  matcher: ['/((?!api|_next/static|_next/image|favicon.ico|sitemap.xml|robots.txt).*)'],
}

4. No external fetch() calls in middleware. If you need external data, pre-compute it and store in edge KV or cookies. Middleware reads, it doesn't fetch.

5. Use waitUntil() for non-blocking side effects. Analytics, logging, and telemetry go in waitUntil() — they don't block the response.

6. Composition is a necessary hack. Until Request Interceptors ship, use a chain pattern. But fight to keep each function under 10 lines.

7. The v16 proxy.ts rename is a message — listen to it. "Proxy" means network boundary. If you're doing computation, you're in the wrong file. Move it to a Route Handler or Server Component.

The Decision Matrix

"I need to..."Where to put it
Redirect unauthenticated usersMiddleware (cookie check) + Server Component (token verify)
A/B test without flickerMiddleware (cookie read + rewrite)
Route by subdomainMiddleware (Host header → rewrite)
Set CSP / security headersMiddleware
Detect locale and redirectMiddleware (Accept-Language → rewrite)
Rate limit by IPMiddleware + edge KV (Upstash)
Verify JWT claimsServer Component or Route Handler
Query database for user dataServer Component
Handle form submissionsServer Actions
Static redirects (old URL → new URL)next.config.js redirects (zero overhead)
Complex business logicRoute Handler or Server Component
Evaluate feature flagsBackground job writes to KV → Middleware reads KV

最終裁決:Middleware 最佳實踐清單

經過五個回合的激烈辯論,小組最終匯聚出一份所有六位專家都背書的實用清單。

黃金法則

1. Middleware 是薄閘道,不是應用層。 Redirect、rewrite、設定 headers。如果你的邏輯塞不進 30 行,它可能就不該在 middleware 裡。

2. 永遠不要信任 middleware 作為唯一的安全邊界。 CVE-2025-29927 之後:三層防禦——middleware → Server Component → Data Access Layer。沒有例外。

3. 使用 matcher config,而不是條件邏輯。 靜態 matcher 讓框架完全跳過調用。始終排除靜態資源。始終排除你的重導向目標。

ts
export const config = {
  matcher: ['/((?!api|_next/static|_next/image|favicon.ico|sitemap.xml|robots.txt).*)'],
}

4. 不要在 middleware 中進行外部 fetch() 呼叫。 如果你需要外部資料,預先計算並存儲在 edge KV 或 cookies 中。Middleware 讀取,不 fetch。

5. 使用 waitUntil() 處理非阻塞副作用。 分析、日誌和遙測放在 waitUntil() 裡——它們不阻塞回應。

6. 組合是必要的 hack。 在 Request Interceptors 推出之前,使用 chain 模式。但努力讓每個函式保持在 10 行以內。

7. v16 的 proxy.ts 重命名是一個訊息——聽它的。 「Proxy」意味著網路邊界。如果你在做計算,你在錯誤的檔案裡。移到 Route Handler 或 Server Component。

決策矩陣

「我需要⋯⋯」放在哪裡
重導向未認證用戶Middleware(cookie 檢查)+ Server Component(token 驗證)
無閃爍 A/B 測試Middleware(cookie 讀取 + rewrite)
按子域名路由Middleware(Host 標頭 → rewrite)
設定 CSP / 安全標頭Middleware
偵測語言並重導向Middleware(Accept-Language → rewrite)
按 IP 限速Middleware + edge KV(Upstash)
驗證 JWT 聲明Server Component 或 Route Handler
查詢資料庫獲取用戶資料Server Component
處理表單提交Server Actions
靜態重導向(舊 URL → 新 URL)next.config.js redirects(零開銷)
複雜業務邏輯Route Handler 或 Server Component
評估 feature flags背景任務寫入 KV → Middleware 讀取 KV

Complete Vote Summary

TopicResultScore
Core legitimate use casesThin gateway: redirects, rewrites, headers, cookie-based routing6-0 consensus
JWT verification in middlewareSituation-dependent; must have data layer backup3-3 tie
Async operations in middlewareAvoid; sub-5ms edge KV reads are the exception4-2
Single-file limitationMajority considers it a flaw4-2
proxy.ts Node.js runtime misuse riskMajority predicts more misuse4-2
Three-layer auth defenseMandatory6-0 consensus

The panel's closing statement was unanimous: Next.js Middleware is not dying — it's being right-sized. The trajectory from middleware.ts (Edge, do everything) to proxy.ts (Node.js, do as little as possible) reflects a maturing framework learning where the boundaries should be. The best middleware is the one you barely notice is there.

完整投票總結

議題結果比分
核心合法用例薄閘道:redirects、rewrites、headers、基於 cookie 的路由6-0 共識
在 middleware 中驗證 JWT取決於情境;必須有 data layer 備份3-3 平手
middleware 中的 async 操作避免;低於 5ms 的 edge KV 讀取是例外4-2
單一檔案限制多數認為是缺陷4-2
proxy.ts Node.js runtime 誤用風險多數預測會有更多誤用4-2
三層認證防禦強制性6-0 共識

小組的結語是一致的:Next.js Middleware 不是在消亡——它正在被合理化。middleware.ts(Edge,做所有事)到 proxy.ts(Node.js,做越少越好)的軌跡反映了一個成熟框架在學習邊界應該在哪裡。最好的 middleware 是你幾乎不會注意到它存在的那個。