EP.34Web 開發系列

Next.js Middleware 與 Edge Runtime:
在邊緣執行你的邏輯

Middleware 攔截請求、Edge Functions、Geolocation 分流 — 讓你的應用響應速度提升到 50ms 以內

Joseph Chen 2026 15 min read Middleware · Edge · A/B Testing · Rate Limiting

Section 1:什麼是 Middleware?為什麼需要它?

Next.js Middleware 是每個請求進入你的 Next.js 應用之前的「守門員」——一段在 Edge Runtime 上執行的程式碼,能在請求到達 Page 或 API Route 之前攔截、修改、或重導向它。由於執行在邊緣節點(離用戶最近的伺服器),延遲通常在 50ms 以內,遠優於傳統 Server-Side 邏輯。

請求處理流程

Browser Request[Next.js Middleware]← 在這裡決定:重導向?修改 Header?判斷身份?
Next.js Pages/API Routes

適合在 Middleware 做的事

身份驗證

在請求到達 Page 之前就判斷用戶是否登入,未登入直接重導向到 /login,不需要在每個 Page 重複寫這個邏輯。

A/B Testing

隨機將用戶分配到 A 或 B 組,用 URL Rewrite 讓他們看到不同版本的頁面,URL 對用戶透明,資料收集由 Middleware 處理。

地理分流(Geolocation)

根據用戶的 IP 位址判斷所在國家,自動導向對應地區的版本(台灣 → /tw、日本 → /jp),無需用戶手動切換。

速率限制(Rate Limiting)

在 Edge 層就擋掉過多請求,保護你的 API 不被濫用,而不需要讓請求跑到 Origin Server 才被擋下。

語言偵測

讀取 Accept-Language Header,自動導向用戶語言的版本(zh-TW → /tw、en → /en),提升國際化體驗。

請求/回應修改

在請求中注入自訂 Header(如 x-user-id、x-country),讓後面的 Page 和 API Route 能直接讀取,無需重複驗證。

Middleware vs API Route vs Server Component — 怎麼選?

Middleware每個請求都要執行的邏輯(驗證、日誌、分流)
API Route / Server Action特定的資料操作或複雜的業務邏輯
Server Component首次渲染時需要資料的頁面邏輯

Section 2:基礎 Middleware 語法

Middleware 的入口是專案根目錄下的 middleware.ts(與 app/ 資料夾同層),匯出一個 middleware 函式和一個 config(用來控制哪些路徑觸發 Middleware)。

typescript
// middleware.ts(放在專案根目錄,與 app/ 同層)
import { NextResponse } from 'next/server';
import type { NextRequest } from 'next/server';

export function middleware(request: NextRequest) {
  const { pathname, searchParams } = request.nextUrl;

  console.log(`[Middleware] ${request.method} ${pathname}`);

  // 1. 讀取 Cookie
  const token = request.cookies.get('auth-token')?.value;

  // 2. 讀取 Header
  const country = request.headers.get('x-vercel-ip-country');

  // 3. 直接放行(繼續往下)
  return NextResponse.next();

  // 或者 4. 重導向(URL 改變,瀏覽器發出新請求)
  // return NextResponse.redirect(new URL('/login', request.url));

  // 或者 5. 改寫路徑(URL 不變,但實際渲染不同 page)
  // return NextResponse.rewrite(new URL('/dashboard-v2', request.url));
}

// 控制哪些路徑會觸發 Middleware
export const config = {
  matcher: [
    // 只匹配這些路徑
    '/dashboard/:path*',
    '/admin/:path*',
    '/api/protected/:path*',
    // 排除靜態資源(避免 Middleware 處理 _next/static 等不必要的請求)
    '/((?!_next/static|_next/image|favicon.ico).*)',
  ],
};

NextResponse.next()

放行請求,繼續正常處理。可以在呼叫前修改 request headers,將資訊傳遞給 Page/API Route。

NextResponse.redirect()

重導向到新 URL,瀏覽器會收到 307/308 回應並發出新請求。URL 會改變,用於身份驗證失敗等情境。

NextResponse.rewrite()

在不改變瀏覽器 URL 的情況下,渲染不同的 Page。用於 A/B Testing、地理分流等需要透明切換的場景。

matcher 的撰寫技巧

  • /dashboard/:path* — 匹配 /dashboard 及所有子路徑
  • /((?!_next).*) — 排除以 _next 開頭的路徑(靜態資源)
  • • 把最常觸發的路徑過濾器放在最前面,節省 Middleware 執行資源
  • • 靜態資源(圖片、字體)進入 Middleware 毫無意義,務必排除

Section 3:身份驗證 Middleware

把身份驗證邏輯放在 Middleware 是最理想的做法:一處集中管理,保護所有需要登入的路徑,不需要在每個 Page 或 API Route 重複撰寫。用戶嘗試存取受保護頁面時,Middleware 會在請求到達 Page 之前就判斷並重導向,避免任何受保護內容被渲染。

為什麼在 Middleware 用 jose 而不是 jsonwebtoken?

Middleware 執行在 Edge Runtime,Edge Runtime 不支援 Node.js 的原生 crypto 模組,而 jsonwebtoken 依賴它。jose 是專為 Web 標準 Crypto API 設計的 JWT 程式庫,完全相容 Edge Runtime。

typescript
// middleware.ts
import { NextResponse } from 'next/server';
import type { NextRequest } from 'next/server';
import { jwtVerify } from 'jose';

const JWT_SECRET = new TextEncoder().encode(process.env.JWT_SECRET);

const PUBLIC_PATHS = ['/', '/login', '/register', '/blog'];
const AUTH_PATHS = ['/dashboard', '/profile', '/settings'];

export async function middleware(request: NextRequest) {
  const { pathname } = request.nextUrl;

  // 公開路徑直接放行
  if (PUBLIC_PATHS.some((path) => pathname.startsWith(path))) {
    return NextResponse.next();
  }

  // 需要身份驗證的路徑
  if (AUTH_PATHS.some((path) => pathname.startsWith(path))) {
    const token = request.cookies.get('auth-token')?.value;

    if (!token) {
      // 未登入 → 導向 login,並記住原始路徑
      const loginUrl = new URL('/login', request.url);
      loginUrl.searchParams.set('callbackUrl', pathname);
      return NextResponse.redirect(loginUrl);
    }

    try {
      // 驗證 JWT(在 Edge Runtime 中用 jose,不能用 jsonwebtoken)
      const { payload } = await jwtVerify(token, JWT_SECRET);

      // 將用戶 ID 加入 header,讓後面的 API 可以讀取
      const response = NextResponse.next();
      response.headers.set('x-user-id', payload.sub as string);
      response.headers.set('x-user-role', payload.role as string);
      return response;

    } catch {
      // Token 無效 → 清除 cookie 並導向 login
      const response = NextResponse.redirect(new URL('/login', request.url));
      response.cookies.delete('auth-token');
      return response;
    }
  }

  return NextResponse.next();
}

export const config = {
  matcher: ['/((?!_next/static|_next/image|favicon.ico|api/auth).*)'],
};

在 Page 中讀取 Middleware 注入的 Header

typescript
// app/dashboard/page.tsx(Server Component)
import { headers } from 'next/headers';

export default async function DashboardPage() {
  const headersList = await headers();

  // 讀取 Middleware 注入的用戶資訊
  const userId = headersList.get('x-user-id');
  const userRole = headersList.get('x-user-role');

  // 直接使用,不需要再驗證一次 JWT
  const user = await getUserById(userId!);

  return (
    <div>
      <h1>Welcome, {user.name}</h1>
      {userRole === 'admin' && <AdminPanel />}
    </div>
  );
}

Section 4:A/B Testing 與 Geolocation 分流

Middleware 的 URL Rewrite 能力讓 A/B Testing 和地理分流的實作變得優雅:用戶看到的 URL 不變,但實際渲染的頁面可以完全不同。這比傳統的 Client-Side A/B Testing(用 JavaScript 在前端切換)更可靠——不會有 Flash of Original Content(FOUC)問題,也不會被 Ad Blocker 干擾。

A/B Testing 實作

typescript
// middleware.ts
import { NextResponse } from 'next/server';
import type { NextRequest } from 'next/server';

export function middleware(request: NextRequest) {
  // A/B Test:隨機將用戶分配到 A 或 B 組
  const abGroup = request.cookies.get('ab-group')?.value;

  if (request.nextUrl.pathname === '/pricing') {
    const group = abGroup ?? (Math.random() < 0.5 ? 'A' : 'B');

    // 改寫路徑(用戶看到的 URL 還是 /pricing)
    const response = NextResponse.rewrite(
      new URL(`/pricing-${group}`, request.url)
    );

    // 記住這次的分組(7天),確保用戶每次看到同一個版本
    if (!abGroup) {
      response.cookies.set('ab-group', group, {
        maxAge: 60 * 60 * 24 * 7,
        httpOnly: true,
        sameSite: 'lax',
      });
    }

    return response;
  }
}

你需要額外建立的頁面

  • app/pricing-A/page.tsx — A 組看到的定價頁(例如:強調年費方案)
  • app/pricing-B/page.tsx — B 組看到的定價頁(例如:強調月費方案)
  • • 兩個頁面的 URL 對用戶透明,他們看到的都是 /pricing

Geolocation 分流(Vercel Edge Network 提供)

部署在 Vercel 上時,Edge Network 會自動在每個請求中注入地理位置相關的 Header,你在 Middleware 中可以直接讀取,不需要額外的 IP 查詢服務。

typescript
export function middleware(request: NextRequest) {
  // Vercel 自動注入地理位置 header
  const country = request.headers.get('x-vercel-ip-country') ?? 'US';
  const city = request.headers.get('x-vercel-ip-city');

  // 根據國家導向不同地區的頁面
  if (request.nextUrl.pathname === '/') {
    if (country === 'TW') {
      return NextResponse.rewrite(new URL('/tw', request.url));
    }
    if (country === 'JP') {
      return NextResponse.rewrite(new URL('/jp', request.url));
    }
  }

  // 將地理資訊傳遞給 page components
  const response = NextResponse.next();
  response.headers.set('x-country', country);
  if (city) response.headers.set('x-city', city);
  return response;
}

Vercel 提供的 Geo Headers

  • x-vercel-ip-country — 國家代碼(TW、JP、US)
  • x-vercel-ip-country-region — 地區代碼
  • x-vercel-ip-city — 城市名稱
  • x-vercel-ip-latitude — 緯度
  • x-vercel-ip-longitude — 經度

常見 Geolocation 應用場景

  • • 自動切換語言版本
  • • 顯示當地貨幣與定價
  • • 合規性(GDPR / CCPA 通知)
  • • 內容授權地區限制
  • • 當地特色活動或促銷

Section 5:Edge Runtime 限制與優化

Edge Runtime 是一個輕量化的 JavaScript 執行環境,基於 Web 標準 API(而非 Node.js)。它讓程式碼能在 Cloudflare Workers、Vercel Edge Functions 等邊緣節點執行,全球延遲通常低於 50ms。但輕量化意味著限制——並非所有 Node.js 模組都能在 Edge Runtime 中使用。

❌ 不能在 Edge Runtime 中使用

  • • Node.js fs 模組(檔案系統)
  • • Node.js net 模組(TCP 連線)
  • • jsonwebtoken(依賴 Node.js crypto)
  • • bcrypt(原生 addon)
  • • 大型 npm 包(bundle size 限制)
  • • 直接連接資料庫(Prisma Client 不支援)

✅ 可以在 Edge Runtime 中使用

  • • fetch API(HTTP 請求)
  • • Web Crypto API(加密)
  • • URL、URLSearchParams
  • • TextEncoder / TextDecoder
  • • jose(Edge 相容的 JWT 庫)
  • • cookies()、headers() API

明確指定 Edge Runtime

typescript
// 在 Middleware 中預設就是 Edge Runtime
// 不需要額外宣告

// 明確指定 Edge Runtime(用於 API Route):
export const runtime = 'edge';

// 在 API Route 中同樣可以指定:
// app/api/fast-route/route.ts
export const runtime = 'edge';

export async function GET(request: Request) {
  // 這個 API 會在 Cloudflare Workers / Vercel Edge 上跑
  // 全球延遲 < 50ms
  return Response.json({ message: 'From the edge!' });
}

// ❌ 在 Edge Runtime 中不能用這些:
// - Node.js fs 模組
// - Node.js net 模組
// - jsonwebtoken(因為用了 Node.js crypto)
// - 大型 npm 包(bundle size 有限制)

// ✅ 可以用這些:
// - fetch API
// - Web Crypto API
// - URL、URLSearchParams
// - TextEncoder / TextDecoder
// - jose(JWT 的 Edge 相容版本)
// - cookies、headers

Edge Runtime 的資料庫存取方案

Middleware 中不能直接連接傳統資料庫(因為 TCP 連線問題)。如果需要在邊緣層做基於資料庫的決策,有幾個常見的替代方案:

Upstash Redis

基於 HTTP 的 Redis,完全相容 Edge Runtime。適合快取、Session 儲存、Rate Limiting 計數器等需要低延遲讀寫的場景。

Cloudflare KV / D1

Cloudflare 提供的邊緣儲存方案。KV 是鍵值存儲,D1 是 SQLite 資料庫,都可以在 Edge Worker 中直接存取。

JWT / Cookie(無狀態)

將必要資訊(用戶 ID、角色、權限)編碼在 JWT 中,Middleware 直接解析 JWT 取得資訊,完全不需要資料庫查詢。


Section 6:Rate Limiting 在 Middleware 中

速率限制(Rate Limiting)是 API 安全的基本防護,能夠防止暴力破解、DoS 攻擊、以及過度使用 API 的行為。在 Middleware 中實作意味著惡意請求在到達 Origin Server 之前就被擋下,節省運算資源。搭配 Upstash Redis 的 HTTP API,可以在 Edge Runtime 中完整實作滑動視窗計數。

Rate Limiting 演算法比較

Fixed Window

每個固定時間視窗(如每分鐘)重置計數器,實作簡單但在視窗邊界可能被突破。

Sliding Window

根據最近 N 秒內的請求數量限制,比 Fixed Window 更平滑,Upstash 推薦方案。

Token Bucket

以固定速率補充 Token,允許短暫的突發流量,適合需要彈性的 API。

用 Upstash Redis 實作邊緣層速率限制

typescript
// middleware.ts
import { Ratelimit } from '@upstash/ratelimit';
import { Redis } from '@upstash/redis';
import { NextResponse } from 'next/server';
import type { NextRequest } from 'next/server';

const ratelimit = new Ratelimit({
  redis: Redis.fromEnv(),
  limiter: Ratelimit.slidingWindow(10, '10 s'), // 10秒內最多10次
  analytics: true,
});

export async function middleware(request: NextRequest) {
  if (request.nextUrl.pathname.startsWith('/api/')) {
    // 用 IP 作為限流 key
    const ip = request.headers.get('x-forwarded-for') ?? '127.0.0.1';
    const { success, limit, remaining, reset } = await ratelimit.limit(ip);

    if (!success) {
      return new NextResponse(
        JSON.stringify({ error: 'Too many requests' }),
        {
          status: 429,
          headers: {
            'Content-Type': 'application/json',
            'X-RateLimit-Limit': limit.toString(),
            'X-RateLimit-Remaining': remaining.toString(),
            'X-RateLimit-Reset': new Date(reset).toISOString(),
            'Retry-After': Math.ceil((reset - Date.now()) / 1000).toString(),
          },
        }
      );
    }
  }

  return NextResponse.next();
}

export const config = {
  matcher: ['/api/:path*'],
};

根據用戶角色設定不同的限流規則

typescript
// 進階:依用戶角色動態設定 Rate Limit
export async function middleware(request: NextRequest) {
  if (!request.nextUrl.pathname.startsWith('/api/')) {
    return NextResponse.next();
  }

  const token = request.cookies.get('auth-token')?.value;
  let userRole = 'anonymous';

  // 解析 JWT 取得用戶角色(不需要資料庫查詢)
  if (token) {
    try {
      const { payload } = await jwtVerify(token, JWT_SECRET);
      userRole = payload.role as string;
    } catch {
      // Token 無效,視為匿名用戶
    }
  }

  // 不同角色的限流規則
  const limits = {
    admin: Ratelimit.slidingWindow(1000, '1 m'),    // 管理員:每分鐘 1000 次
    premium: Ratelimit.slidingWindow(200, '1 m'),   // 付費用戶:每分鐘 200 次
    user: Ratelimit.slidingWindow(60, '1 m'),       // 一般用戶:每分鐘 60 次
    anonymous: Ratelimit.slidingWindow(10, '1 m'),  // 匿名:每分鐘 10 次
  };

  const limiter = limits[userRole as keyof typeof limits] ?? limits.anonymous;

  const ratelimit = new Ratelimit({
    redis: Redis.fromEnv(),
    limiter,
  });

  const ip = request.headers.get('x-forwarded-for') ?? '127.0.0.1';
  const key = `${userRole}:${ip}`; // 用角色+IP 組合 key
  const { success, remaining } = await ratelimit.limit(key);

  if (!success) {
    return NextResponse.json(
      { error: 'Rate limit exceeded. Please upgrade your plan for higher limits.' },
      { status: 429 }
    );
  }

  // 把剩餘次數傳給 API 方便 debug
  const response = NextResponse.next();
  response.headers.set('X-RateLimit-Remaining', remaining.toString());
  return response;
}

Rate Limiting 最佳實踐

  • • 回傳標準的 Retry-After Header
  • • 在回應中說明限流原因
  • • 對付費用戶提供更高的上限
  • • 記錄被擋下的請求以偵測攻擊

常見陷阱

  • • 用 IP 限流時,NAT 後的辦公室可能共用 IP
  • • x-forwarded-for 可以偽造,高安全場景需要額外驗證
  • • 不要在 Middleware 中做太重的計算,否則失去邊緣加速的優勢

Middleware 是 Next.js 全端架構的最後一塊拼圖

把 Middleware 加入你的架構思維後,整個 Next.js 應用的請求處理層次就完整了:Edge Middleware(守門員)→ Server Components(資料獲取)→ Client Components(互動)→ Server Actions(資料變更)。每一層都有明確的職責,整體架構清晰而高效。


Next.jsMiddlewareEdge RuntimeA/B TestingRate LimitingGeolocationAuthentication