Next.js 16 Middleware: คู่มือฉบับสมบูรณ์ (proxy.ts, Auth, Rate Limit) ปี 2026

คู่มือใช้ Next.js 16 Middleware ผ่าน proxy.ts สำหรับ Authentication, Redirects, Rate Limiting, Geo-routing และเลือก Edge vs Node Runtime อย่างเหมาะสม พร้อมโค้ดตัวอย่างพร้อม copy ไปใช้

Next.js 16 Middleware Guide (2026)

อัปเดต: 21 มิถุนายน 2026

Next.js 16 Middleware คือโค้ดที่รันก่อนคำขอ (request) จะถึงหน้าเว็บหรือ Route Handler ของคุณ ใช้สำหรับตรวจสอบ Authentication, ทำ Redirects, ปรับแต่ง Headers, จำกัดอัตราการเรียก (Rate Limiting) และ Geo-routing โดยใน Next.js 16 มีการเปลี่ยนแปลงสำคัญคือ Middleware รันบน Node.js Runtime เป็นค่าเริ่มต้นแทนที่จะเป็น Edge Runtime ทำให้คุณใช้ Native Node APIs และ npm packages ได้เต็มที่ ผมเองเพิ่ง migrate โปรเจกต์ production สองตัวจาก Edge มาเป็น Node middleware เมื่อเดือนที่แล้ว เลยอยากแชร์ pattern ที่ใช้งานได้จริง พร้อมโค้ดที่คุณ copy ไปใช้ได้เลย

  • Next.js 16 ใช้ Node.js Runtime เป็นค่าเริ่มต้นใน Middleware ทำให้เข้าถึง fs, crypto และ npm packages ที่พึ่ง Node APIs ได้โดยตรง
  • ไฟล์ middleware.ts ถูกแทนที่ด้วย proxy.ts ที่ root ของโปรเจกต์ใน Next.js 16 (ชื่อเดิมยังใช้ได้แต่จะ deprecated)
  • matcher config ช่วยจำกัดเส้นทางที่ middleware จะรัน ซึ่งสำคัญต่อ performance (รันทุก request ที่ไม่กรองจะทำให้ TTFB ช้าลง)
  • ใช้ NextResponse.redirect(), NextResponse.rewrite(), และ NextResponse.next() เพื่อควบคุมการตอบสนอง
  • Rate limiting ที่เหมาะสมควรใช้ Redis (เช่น Upstash) ไม่ใช่ in-memory Map เพราะ instance หลายตัวจะไม่แชร์ state
  • Middleware ไม่ควรทำงานหนัก เช่น query database โดยตรง ให้ตรวจ session token เท่านั้น และ defer การ verify ไปที่ Server Component

Next.js 16 Middleware คืออะไรและทำงานอย่างไร

Middleware ใน Next.js 16 คือ JavaScript function ที่ทำงานระหว่างเซิร์ฟเวอร์ได้รับ HTTP request กับการ render หน้าเว็บ เปรียบเหมือนยามหน้าประตูที่ตรวจสอบทุกคำขอก่อนปล่อยเข้าไปข้างใน ตัวอย่างที่พบบ่อย เช่น เช็คว่า user login หรือยัง, redirect ไปยัง locale ที่ถูกต้องตามภาษาเบราว์เซอร์, เพิ่ม security headers (CSP, HSTS), หรือบล็อก IP ที่อยู่ใน blacklist

เมื่อ request เข้ามา Next.js จะตรวจสอบว่า path ตรงกับ matcher ที่กำหนดหรือไม่ ถ้าตรงก็จะรัน middleware function ก่อน function จะตัดสินใจว่าจะปล่อยให้ไปต่อ (NextResponse.next()), redirect, rewrite, หรือ block ทันทีพร้อมส่ง response กลับ การที่ middleware ทำงาน "ก่อน" routing ทำให้เหมาะสำหรับ cross-cutting concerns ที่ต้องใช้ในหลายหน้า เช่น auth gate หรือ logging

สิ่งสำคัญที่ต้องเข้าใจคือ middleware รันทุกครั้งที่มี request matching รวมถึง static assets, image optimization, prefetch requests ของ Next.js Link เอง ดังนั้นการ filter ด้วย matcher ให้ดีจึงเป็นเรื่องของ performance ไม่ใช่แค่ความสวยงามของโค้ด ตามเอกสารทางการ Next.js Middleware Documentation middleware ที่ดีควรทำงานเสร็จภายในไม่กี่มิลลิวินาที

proxy.ts vs middleware.ts: มีอะไรเปลี่ยนใน Next.js 16

ใน Next.js 16 ทีมงาน Vercel ได้เปลี่ยนชื่อไฟล์ middleware เป็น proxy.ts (หรือ proxy.js) วางที่ root ของโปรเจกต์ เหตุผลคือชื่อ "middleware" ทำให้หลายคนเข้าใจผิดว่าเป็น Express-style middleware chain แต่จริงๆ Next.js รันเพียง function เดียวต่อ request ชื่อ "proxy" สื่อความหมายชัดเจนกว่าว่าเป็น reverse proxy layer ที่อยู่หน้าแอป

การ migrate ทำได้ง่ายมาก เพียงเปลี่ยนชื่อไฟล์ ส่วน API ทุกอย่างยังเหมือนเดิม:

// proxy.ts (Next.js 16+)
import { NextResponse } from 'next/server'
import type { NextRequest } from 'next/server'

export function proxy(request: NextRequest) {
  const response = NextResponse.next()
  response.headers.set('x-custom-header', 'hello')
  return response
}

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

นอกจากชื่อไฟล์แล้ว Next.js 16 ยังเปลี่ยน default runtime จาก Edge เป็น Node.js ซึ่งเป็นการเปลี่ยนแปลงที่ส่งผลกระทบมากกว่าแค่เปลี่ยนชื่อไฟล์ เราจะลงรายละเอียดในหัวข้อถัดไป

Node.js Runtime vs Edge Runtime: เลือกอันไหนดี

ความแตกต่างหลักระหว่างสอง runtime นี้คือ Node.js Runtime รันบน V8 พร้อม Node APIs เต็มรูปแบบ (เช่น fs, crypto, buffer) ขณะที่ Edge Runtime เป็น subset ของ Web APIs ที่รันบน V8 isolates มีข้อจำกัดมากกว่าแต่ cold start เร็วและรันใกล้ user (CDN edge)

คุณสมบัติNode.js RuntimeEdge Runtime
Cold start50–200ms5–20ms
Memory limit1024MB+128MB
npm packagesใช้ได้ทั้งหมดเฉพาะที่รองรับ Web APIs
Node APIs (fs, crypto)ใช้ได้ใช้ไม่ได้
Bundle size limitไม่จำกัด1MB (Vercel)
การวิ่งใกล้ userRegion เดียวGlobal edge
เหมาะกับDB query, file ops, npm-heavyAuth check, geo, header rewriting

สำหรับ Next.js 16 ทีมงานเปลี่ยน default เป็น Node.js เพราะว่าผู้ใช้ส่วนใหญ่ติดปัญหากับข้อจำกัดของ Edge เช่น ไม่สามารถ import package ที่ใช้ crypto.randomBytes ได้ หรือ bundle เกิน 1MB เพราะ dependencies ของ Auth library ตอนผมเจอ error "Module not found: crypto" บน Edge ครั้งแรก เสียเวลา debug ไปครึ่งวันก่อนจะพบว่าเป็นเพราะ runtime ไม่รองรับ ถ้าคุณต้องการ Edge Runtime ให้ระบุชัดเจน:

// proxy.ts
export const config = {
  runtime: 'edge', // explicit opt-in
  matcher: '/((?!api|_next).*)',
}

สำหรับโครงการที่ deploy บน Vercel การใช้ Edge ยังคงคุ้มค่าหากคุณทำเพียง JWT verify หรือ A/B test routing เพราะลด TTFB ได้มาก ดูข้อมูลเพิ่มเติมที่ Vercel Edge Runtime documentation

การทำ Authentication ด้วย Middleware

หนึ่งใน use case ที่พบบ่อยที่สุดคือการป้องกันหน้าที่ต้อง login เช่น /dashboard, /settings middleware เหมาะอย่างยิ่งเพราะรันก่อน page render ทำให้ผู้ใช้ที่ไม่ได้ login จะถูก redirect ไปหน้า /login โดยไม่ต้องโหลด client bundle ของหน้าที่ป้องกันก่อน

หลักการสำคัญคือใน middleware ให้ตรวจเพียง "session token มีอยู่และยังไม่หมดอายุ" เท่านั้น อย่า query database เพราะจะทำให้ทุก request ช้า ส่วนการ verify ลึก (เช่น token revoked หรือไม่) ให้ทำใน Server Component หรือ Route Handler หากต้องการเรียนรู้การตั้งค่า Auth.js v5 อย่างละเอียด ดูได้ที่ คู่มือ Auth.js v5 พร้อม Google OAuth และ RBAC

// proxy.ts
import { NextResponse } from 'next/server'
import type { NextRequest } from 'next/server'
import { jwtVerify } from 'jose'

const PROTECTED_PATHS = ['/dashboard', '/settings', '/admin']
const SECRET = new TextEncoder().encode(process.env.JWT_SECRET!)

export async function proxy(request: NextRequest) {
  const { pathname } = request.nextUrl
  const needsAuth = PROTECTED_PATHS.some(p => pathname.startsWith(p))
  if (!needsAuth) return NextResponse.next()

  const token = request.cookies.get('session')?.value
  if (!token) {
    const loginUrl = new URL('/login', request.url)
    loginUrl.searchParams.set('from', pathname)
    return NextResponse.redirect(loginUrl)
  }

  try {
    await jwtVerify(token, SECRET)
    return NextResponse.next()
  } catch {
    // Token invalid or expired
    const response = NextResponse.redirect(new URL('/login', request.url))
    response.cookies.delete('session')
    return response
  }
}

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

Redirects และ Rewrites: ความแตกต่างและการใช้งาน

หลายคนสับสนระหว่าง redirect กับ rewrite ความแตกต่างคือ redirect เปลี่ยน URL ที่เบราว์เซอร์เห็น (ส่ง 307/308 status) ขณะที่ rewrite เก็บ URL เดิมไว้ที่เบราว์เซอร์แต่ render เนื้อหาจาก path อื่นภายใน (proxy-style) สำหรับ SEO redirect บอก Google ว่าเนื้อหาย้าย ส่วน rewrite ไม่ส่งสัญญาณนั้น

ตัวอย่างการใช้งานจริง:

// proxy.ts
export function proxy(request: NextRequest) {
  const { pathname } = request.nextUrl

  // Redirect: เปลี่ยน URL ที่ user เห็น (เหมาะกับ deprecated routes)
  if (pathname === '/old-blog') {
    return NextResponse.redirect(new URL('/blog', request.url), 308)
  }

  // Rewrite: เก็บ URL เดิมแต่ serve เนื้อหาจาก path ใหม่
  // เหมาะกับ A/B testing หรือ multi-tenant
  if (pathname.startsWith('/shop')) {
    const variant = request.cookies.get('ab-variant')?.value ?? 'a'
    return NextResponse.rewrite(
      new URL(`/shop-${variant}${pathname.replace('/shop', '')}`, request.url)
    )
  }

  return NextResponse.next()
}

สำหรับ HTTP status code ของ redirect: ใช้ 308 (Permanent) เมื่อต้องการให้ search engine update index และ user agent cache ใช้ 307 (Temporary) เมื่อ redirect แบบชั่วคราว เช่น maintenance mode การส่ง 308 ผิดที่อาจทำให้เกิดปัญหา cached redirect ที่ user ลบไม่ได้ในเบราว์เซอร์ (เคยเจอ user complain เรื่องนี้แล้วต้องบอกให้ clear browser data ทีละคน ไม่ใช่ประสบการณ์ที่อยากให้เกิดอีก)

Rate Limiting ด้วย Upstash Redis

การจำกัดอัตราการเรียก (rate limiting) ป้องกัน abuse และ brute-force attack ใน middleware เราสามารถ implement ได้ทันทีก่อน request ถึง API route ตัวอย่างนี้ใช้ Upstash Redis เพราะมี SDK ที่รองรับทั้ง Edge และ Node Runtime และมี free tier 10,000 commands/วัน ดู API reference เพิ่มเติมได้ที่ Upstash Ratelimit SDK

// proxy.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, '60 s'), // 10 req / 60s
  analytics: true,
  prefix: 'rl:api',
})

export async function proxy(request: NextRequest) {
  if (!request.nextUrl.pathname.startsWith('/api/')) {
    return NextResponse.next()
  }

  const ip = request.headers.get('x-forwarded-for')?.split(',')[0]
    ?? request.headers.get('x-real-ip')
    ?? '127.0.0.1'

  const { success, limit, remaining, reset } = await ratelimit.limit(ip)

  const response = success
    ? NextResponse.next()
    : NextResponse.json(
        { error: 'Too many requests' },
        { status: 429 }
      )

  response.headers.set('X-RateLimit-Limit', limit.toString())
  response.headers.set('X-RateLimit-Remaining', remaining.toString())
  response.headers.set('X-RateLimit-Reset', reset.toString())
  return response
}

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

Geolocation และ i18n Routing

Next.js 16 รองรับ geolocation ผ่าน request.geo (เมื่อ deploy บน Vercel หรือใช้ third-party geo header) ทำให้ middleware สามารถ redirect user ไปยัง locale ที่ถูกต้องอัตโนมัติ หรือบล็อกประเทศที่ไม่อนุญาตให้ใช้บริการ

// proxy.ts
import { NextResponse } from 'next/server'
import type { NextRequest } from 'next/server'

const SUPPORTED_LOCALES = ['en', 'th', 'ja', 'zh']
const DEFAULT_LOCALE = 'en'

const COUNTRY_TO_LOCALE: Record<string, string> = {
  TH: 'th',
  JP: 'ja',
  CN: 'zh',
  HK: 'zh',
  TW: 'zh',
}

export function proxy(request: NextRequest) {
  const { pathname } = request.nextUrl

  // ข้ามถ้ามี locale ใน path แล้ว
  const pathnameHasLocale = SUPPORTED_LOCALES.some(
    l => pathname.startsWith(`/${l}/`) || pathname === `/${l}`
  )
  if (pathnameHasLocale) return NextResponse.next()

  // หา locale จาก cookie ก่อน (user choice)
  let locale = request.cookies.get('NEXT_LOCALE')?.value

  // ถ้าไม่มี ใช้ geo + Accept-Language
  if (!locale) {
    const country = request.geo?.country ?? ''
    locale = COUNTRY_TO_LOCALE[country]
      ?? parseAcceptLanguage(request.headers.get('accept-language'))
      ?? DEFAULT_LOCALE
  }

  const newUrl = new URL(`/${locale}${pathname}`, request.url)
  return NextResponse.redirect(newUrl)
}

function parseAcceptLanguage(header: string | null): string | null {
  if (!header) return null
  const langs = header.split(',').map(s => s.split(';')[0].trim())
  for (const lang of langs) {
    const code = lang.split('-')[0]
    if (SUPPORTED_LOCALES.includes(code)) return code
  }
  return null
}

export const config = {
  matcher: ['/((?!api|_next|.*\\..*).*)'],
}

ลำดับความสำคัญที่แนะนำคือ: 1) explicit user choice (cookie) > 2) geolocation > 3) Accept-Language header > 4) default locale การให้ cookie เป็นอันดับแรกเคารพการเลือกของผู้ใช้ที่อาจอยู่ในประเทศหนึ่งแต่อยากใช้ภาษาอื่น

Matcher Config: จำกัดเส้นทางที่ middleware ทำงาน

การ filter request ด้วย matcher เป็นเรื่อง critical ของ performance ลองนึกถึงเว็บที่มี 100k request/นาที ถ้า middleware ทำงานทุก request รวม static asset (รูป, CSS, JS chunk) คุณจะเสีย 100k × middleware latency ทันที

มี 4 รูปแบบ matcher ที่ใช้บ่อย:

// 1. String literal - ตรงเป๊ะ
export const config = { matcher: '/about' }

// 2. Path with dynamic segment
export const config = { matcher: '/dashboard/:path*' }

// 3. Regex-style negative lookahead (ใช้บ่อยที่สุด)
export const config = {
  matcher: [
    '/((?!api|_next/static|_next/image|favicon.ico|robots.txt|sitemap.xml).*)',
  ],
}

// 4. Multiple matchers with conditional logic
export const config = {
  matcher: [
    {
      source: '/api/:path*',
      has: [{ type: 'header', key: 'x-api-key' }],
    },
    '/dashboard/:path*',
  ],
}

การใช้ negative lookahead เพื่อ exclude paths เป็น pattern ที่ดีกว่า "include everything" + check ใน function เพราะ matcher ทำงานที่ routing layer ก่อน function จะถูก invoke เลย ประหยัดทั้ง compute และ cold start หากคุณกำลังเรียนรู้เรื่อง routing เพิ่มเติม ลองอ่าน คู่มือ Parallel Routes และ Intercepting Routes หรือถ้าสนใจเรื่องจัดการ error ในชั้น app router ผมแนะนำ คู่มือ Error Handling ใน Next.js 16 ด้วย

ข้อผิดพลาดที่พบบ่อยและวิธีแก้

จากการ review code middleware ของหลายโปรเจกต์ ผมพบ pattern ผิดพลาดซ้ำๆ ดังนี้

1. Query database ใน middleware

ทุก request ที่ match จะ trigger DB query เพิ่ม TTFB 50–200ms และเพิ่ม load DB อย่างมาก แก้โดยใช้ JWT verify (stateless) หรือ session cookie ที่มี data ฝังอยู่

2. ไม่ใช้ matcher

middleware รันทุก request รวม /_next/image, /favicon.ico ทำให้แอปช้าและเปลือง edge invocations แก้โดยใช้ negative lookahead matcher ดังตัวอย่างข้างบน

3. ใช้ in-memory rate limiting

เมื่อ scale หลาย instance แต่ละ instance มี Map ของตัวเอง user อาจส่ง 10 requests ไปคนละ instance ผ่านได้หมด แก้โดยใช้ Redis-backed rate limiter

4. Redirect loop

เกิดจาก redirect ไปยัง path ที่ match กับ matcher เดิม ทำให้ middleware รันอีกครั้งและ redirect อีก แก้โดย exclude destination path จาก matcher หรือ check pathname ก่อน redirect

5. ลืม handle async errors

หาก jwtVerify หรือ Redis call throw error ที่ไม่ได้ catch จะทำให้ทุก request fail ห่อด้วย try/catch และ fallback ไป NextResponse.next() เพื่อ fail open (หรือ fail closed ถ้าเป็น security-critical)

คำถามที่พบบ่อย

Middleware ใน Next.js 16 รันบน Edge หรือ Node.js?

ค่าเริ่มต้นใน Next.js 16 คือ Node.js Runtime ซึ่งเปลี่ยนจากเวอร์ชันก่อนหน้าที่เป็น Edge หากต้องการ Edge ให้ตั้ง runtime: 'edge' ใน config ของ proxy.ts

Middleware ต่างจาก API Route อย่างไร?

Middleware รันก่อน routing สำหรับทุก request ที่ match โดยไม่ return เนื้อหาตรงๆ ส่วน API Route (Route Handler) เป็น endpoint สุดทาง return JSON/data ตามที่ client ขอ Middleware เหมาะกับ cross-cutting concerns ส่วน Route Handler เหมาะกับ business logic

ทำไมเปลี่ยนชื่อจาก middleware.ts เป็น proxy.ts?

ทีมงาน Vercel เปลี่ยนเพื่อสื่อความหมายชัดเจนขึ้น เนื่องจากชื่อเดิมทำให้สับสนกับ Express-style middleware chain แต่ Next.js รันเพียง function เดียวต่อ request ชื่อใหม่ "proxy" สะท้อนบทบาทเป็น reverse proxy layer ได้ดีกว่า

Middleware ควรทำงานเร็วแค่ไหน?

เป้าหมายคือต่ำกว่า 50ms ต่อ invocation เพราะมันบวกเข้าไปใน TTFB ของทุก request หากเกิน 100ms ควรพิจารณาย้าย logic หนักไปที่ Server Component หรือ Route Handler และเก็บ middleware ไว้ทำเพียง auth check, redirect, header rewriting

ใช้ database connection pool ใน middleware ได้ไหม?

ไม่แนะนำ เพราะ middleware แต่ละ invocation อาจ run บน instance ใหม่ ทำให้ pool ไม่ persistent และจะกินจำนวน connection อย่างรวดเร็ว ถ้าจำเป็นต้อง query DB ให้ใช้ HTTP-based driver เช่น Neon serverless หรือ Upstash Redis แทน connection pool แบบดั้งเดิม

Editorial Team
เกี่ยวกับผู้เขียน Editorial Team

Our team of expert writers and editors.