Middleware and Edge Functions

Bài 15 – Cách sử dụng Middleware và Edge Functions trong Next.js để xử lý request tại Edge Network với hiệu suất cao, phù hợp cho xác thực, redirect, A/B testing và biến đổi request/response

18/11/2025 DaiPhan
Bài 15 / 18

1. Giới thiệu về Middleware và Edge Functions

Middleware và Edge Functions là những công cụ mạnh mẽ trong Next.js cho phép bạn xử lý request trước khi trang hoặc API được render. Được thực thi tại Edge Network, chúng mang lại tốc độ phản hồi cực kỳ nhanh chóng.

Lợi ích chính:

  • Hiệu suất cao: Thực thi tại edge locations gần người dùng
  • Giảm latency: Xử lý request trước khi đến server chính
  • Khả năng mở rộng: Tự động scale theo nhu cầu
  • Chi phí thấp: Tối ưu cho các tác vụ nhẹ

Các trường hợp sử dụng phổ biến:

  • Xác thực và authorization: Kiểm tra quyền truy cập trước khi vào route
  • Redirect và rewrite: Điều hướng người dùng một cách linh hoạt
  • A/B testing: Phân chia traffic cho các phiên bản khác nhau
  • Internationalization: Xử lý ngôn ngữ dựa trên location
  • Rate limiting: Giới hạn số request từ một IP
  • Header manipulation: Thêm/sửa/xóa headers một cách linh hoạt

2. Nội dung chính

2.1 Khái niệm Middleware trong Next.js

Middleware trong Next.js là một function được chạy trước khi request hoàn thành. Nó cho phép bạn:

  • Chặn hoặc cho phép request tiếp tục
  • Điều hướng người dùng sang route khác
  • Thay đổi request/response
  • Thực thi logic xác thực

Cấu trúc file:

// middleware.ts (hoặc middleware.js) ở root directory
import { NextResponse } from 'next/server'
import type { NextRequest } from 'next/server'

export function middleware(request: NextRequest) {
  // Logic xử lý middleware
  return NextResponse.next()
}

// Cấu hình matcher để chỉ định route nào sẽ áp dụng
export const config = {
  matcher: ['/dashboard/:path*', '/api/protected/:path*']
}

2.2 Edge Runtime: Giới hạn và Khả năng

Edge Runtime là môi trường thực thi tối thiểu dựa trên Web APIs:

Khả năng:

  • Web APIs: Request, Response, Headers, URL
  • Crypto APIs: crypto, TextEncoder, TextDecoder
  • Fetch API và Streams
  • Timers: setTimeout, setInterval

Giới hạn quan trọng:

  • Không có Node.js APIs (fs, path, crypto của Node.js)
  • Không có database drivers truyền thống
  • Execution time giới hạn (thường 30 giây)
  • Memory giới hạn (thường 128MB)

2.3 NextRequest và NextResponse APIs

NextRequest - Mở rộng từ Request object:

const request: NextRequest = req

// Các thuộc tính mở rộng
const pathname = request.nextUrl.pathname
const searchParams = request.nextUrl.searchParams
const cookies = request.cookies
const geo = request.geo // Thông tin location
const ip = request.ip
const ua = request.ua // User agent

NextResponse - Mở rộng từ Response object:

// Redirect
return NextResponse.redirect(new URL('/login', request.url))

// Rewrite (giữ nguyên URL trong browser)
return NextResponse.rewrite(new URL('/dashboard', request.url))

// Next (tiếp tục request)
return NextResponse.next()

// Response với headers tùy chỉnh
const response = NextResponse.next()
response.headers.set('x-custom-header', 'value')
return response

2.4 Edge Functions trong App Router

Edge Functions trong App Router được cấu hình thông qua Route Segment Config:

// app/api/edge-demo/route.ts
export const runtime = 'edge'
export const preferredRegion = 'iad1' // Optional: chỉ định region

export async function GET(request: Request) {
  // Logic xử lý tại edge
  return new Response(JSON.stringify({ 
    message: 'Edge Function',
    timestamp: Date.now(),
    region: process.env.VERCEL_REGION || 'unknown'
  }), {
    headers: { 'content-type': 'application/json' }
  })
}

Lưu ý quan trọng:

  • Không thể sử dụng edge runtime với getStaticProps hoặc getServerSideProps
  • Một số thư viện có thể không tương thích với edge runtime
  • Luôn test kỹ khi triển khai

3. Ví dụ thực tế

3.1 Middleware cho Authentication và Authorization

Ví dụ 1: Basic Authentication Guard

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

export function middleware(request: NextRequest) {
  const token = request.cookies.get('auth-token')
  const isAuthPage = request.nextUrl.pathname.startsWith('/login')
  
  // Nếu đã login và đang ở trang login thì redirect về dashboard
  if (token && isAuthPage) {
    return NextResponse.redirect(new URL('/dashboard', request.url))
  }
  
  // Nếu chưa login và đang truy cập protected route
  if (!token && request.nextUrl.pathname.startsWith('/dashboard')) {
    const loginUrl = new URL('/login', request.url)
    loginUrl.searchParams.set('redirectTo', request.nextUrl.pathname)
    return NextResponse.redirect(loginUrl)
  }
  
  return NextResponse.next()
}

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

Ví dụ 2: Role-based Authorization

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

// Giải mã JWT đơn giản (thực tế nên dùng thư viện như jose)
function decodeJWT(token: string) {
  try {
    const payload = JSON.parse(atob(token.split('.')[1]))
    return payload
  } catch {
    return null
  }
}

export function middleware(request: NextRequest) {
  const token = request.cookies.get('auth-token')?.value
  
  // Admin routes
  if (request.nextUrl.pathname.startsWith('/admin')) {
    if (!token) {
      return NextResponse.redirect(new URL('/login', request.url))
    }
    
    const user = decodeJWT(token)
    if (!user || user.role !== 'admin') {
      return NextResponse.redirect(new URL('/unauthorized', request.url))
    }
  }
  
  // API routes cần authorization
  if (request.nextUrl.pathname.startsWith('/api/admin')) {
    if (!token) {
      return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
    }
    
    const user = decodeJWT(token)
    if (!user || user.role !== 'admin') {
      return NextResponse.json({ error: 'Forbidden' }, { status: 403 })
    }
  }
  
  return NextResponse.next()
}

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

3.2 Middleware cho Internationalization (i18n)

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

const locales = ['en', 'vi', 'ja']
const defaultLocale = 'vi'

function getLocale(request: NextRequest): string {
  // 1. Kiểm tra cookie đã lưu locale trước đó
  const localeCookie = request.cookies.get('locale')
  if (localeCookie && locales.includes(localeCookie.value)) {
    return localeCookie.value
  }
  
  // 2. Kiểm tra Accept-Language header
  const acceptLanguage = request.headers.get('accept-language')
  if (acceptLanguage) {
    const preferredLocale = acceptLanguage.split(',')[0].split('-')[0]
    if (locales.includes(preferredLocale)) {
      return preferredLocale
    }
  }
  
  // 3. Dựa vào geo location (nếu có)
  const country = request.geo?.country
  const countryLocales: Record<string, string> = {
    'VN': 'vi',
    'JP': 'ja'
  }
  
  if (country && countryLocales[country]) {
    return countryLocales[country]
  }
  
  // 4. Mặc định
  return defaultLocale
}

export function middleware(request: NextRequest) {
  const { pathname } = request.nextUrl
  
  // Kiểm tra xem pathname đã có locale chưa
  const pathnameHasLocale = locales.some(
    locale => pathname.startsWith(`/${locale}/`) || pathname === `/${locale}`
  )
  
  if (pathnameHasLocale) return NextResponse.next()
  
  // Redirect về locale phù hợp
  const locale = getLocale(request)
  request.nextUrl.pathname = `/${locale}${pathname}`
  
  const response = NextResponse.redirect(request.nextUrl)
  
  // Lưu locale vào cookie để lần sau dùng
  response.cookies.set('locale', locale, {
    maxAge: 60 * 60 * 24 * 30 // 30 ngày
  })
  
  return response
}

export const config = {
  matcher: [
    // Skip all internal paths (_next)
    '/((?!_next).*)',
    // Optional: only run on root (/) URL
    // '/'
  ]
}

3.3 A/B Testing với Middleware

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

// Cấu hình A/B test
const abTests = {
  'homepage-v2': {
    variants: {
      'control': 0.5, // 50% người dùng thấy phiên bản cũ
      'variant': 0.5   // 50% người dùng thấy phiên bản mới
    }
  }
}

function getVariant(testName: string, userId: string): string {
  const test = abTests[testName as keyof typeof abTests]
  if (!test) return 'control'
  
  // Hash userId để đảm bảo cùng một user luôn thấy cùng một variant
  const hash = userId.split('').reduce((acc, char) => {
    return acc + char.charCodeAt(0)
  }, 0)
  
  const random = (hash % 100) / 100
  let cumulative = 0
  
  for (const [variant, weight] of Object.entries(test.variants)) {
    cumulative += weight
    if (random <= cumulative) {
      return variant
    }
  }
  
  return 'control'
}

export function middleware(request: NextRequest) {
  const { pathname } = request.nextUrl
  
  // A/B test cho homepage
  if (pathname === '/') {
    // Lấy hoặc tạo user ID
    let userId = request.cookies.get('ab-user-id')?.value
    if (!userId) {
      userId = crypto.randomUUID()
    }
    
    const variant = getVariant('homepage-v2', userId)
    
    // Rewrite sang variant tương ứng
    if (variant === 'variant') {
      const response = NextResponse.rewrite(new URL('/homepage-v2', request.url))
      
      // Lưu tracking data
      response.cookies.set('ab-homepage-v2', 'variant', {
        maxAge: 60 * 60 * 24 * 7 // 7 ngày
      })
      
      // Lưu user ID nếu mới tạo
      if (!request.cookies.get('ab-user-id')) {
        response.cookies.set('ab-user-id', userId, {
          maxAge: 60 * 60 * 24 * 365 // 1 năm
        })
      }
      
      return response
    }
  }
  
  return NextResponse.next()
}

export const config = {
  matcher: ['/']
}

3.4 Rate Limiting với Edge Functions

// app/api/rate-limit/route.ts
export const runtime = 'edge'

// Đơn giản hóa: dùng memory store (trong production nên dùng Redis)
const rateLimitStore = new Map<string, { count: number; resetTime: number }>()

const RATE_LIMIT = 10 // 10 requests
const WINDOW_MS = 60 * 1000 // 1 minute

function isRateLimited(ip: string): boolean {
  const now = Date.now()
  const userLimit = rateLimitStore.get(ip)
  
  if (!userLimit || now > userLimit.resetTime) {
    // Reset hoặc tạo mới
    rateLimitStore.set(ip, {
      count: 1,
      resetTime: now + WINDOW_MS
    })
    return false
  }
  
  if (userLimit.count >= RATE_LIMIT) {
    return true
  }
  
  userLimit.count++
  return false
}

export async function GET(request: Request) {
  const ip = request.headers.get('x-forwarded-for') || 'unknown'
  
  if (isRateLimited(ip)) {
    return new Response(JSON.stringify({ 
      error: 'Too many requests',
      retryAfter: Math.ceil(WINDOW_MS / 1000)
    }), {
      status: 429,
      headers: {
        'content-type': 'application/json',
        'retry-after': Math.ceil(WINDOW_MS / 1000).toString()
      }
    })
  }
  
  return new Response(JSON.stringify({ 
    message: 'Success',
    remaining: RATE_LIMIT - (rateLimitStore.get(ip)?.count || 0)
  }), {
    headers: { 'content-type': 'application/json' }
  })
}

3.5 Edge Function cho Real-time Data

// app/api/weather-edge/route.ts
export const runtime = 'edge'
export const preferredRegion = 'sin1' // Singapore - gần Việt Nam

interface WeatherData {
  temperature: number
  humidity: number
  condition: string
  location: string
}

// Cache đơn giản (trong production dùng cache service)
const cache = new Map<string, { data: WeatherData; timestamp: number }>()
const CACHE_DURATION = 5 * 60 * 1000 // 5 phút

async function getWeatherData(city: string): Promise<WeatherData> {
  const cached = cache.get(city)
  const now = Date.now()
  
  if (cached && (now - cached.timestamp) < CACHE_DURATION) {
    return cached.data
  }
  
  // Giả lập API call (thực tế dùng OpenWeatherMap, v.v.)
  const weatherData: WeatherData = {
    temperature: Math.round(Math.random() * 40),
    humidity: Math.round(Math.random() * 100),
    condition: ['Sunny', 'Cloudy', 'Rainy'][Math.floor(Math.random() * 3)],
    location: city
  }
  
  cache.set(city, { data: weatherData, timestamp: now })
  return weatherData
}

export async function GET(request: Request) {
  const { searchParams } = new URL(request.url)
  const city = searchParams.get('city') || 'Hanoi'
  
  try {
    const weatherData = await getWeatherData(city)
    
    return new Response(JSON.stringify({
      success: true,
      data: weatherData,
      cachedAt: new Date().toISOString(),
      region: process.env.VERCEL_REGION || 'unknown'
    }), {
      headers: {
        'content-type': 'application/json',
        'cache-control': 'public, s-maxage=300' // Cache 5 phút
      }
    })
  } catch (error) {
    return new Response(JSON.stringify({
      success: false,
      error: 'Failed to fetch weather data'
    }), {
      status: 500,
      headers: { 'content-type': 'application/json' }
    })
  }
}

4. Kiến thức trọng tâm

4.1 Best Practices cho Middleware

1. Performance Optimization:

  • Luôn sử dụng config.matcher để giới hạn scope
  • Tránh xử lý nặng trong middleware
  • Cache kết quả khi có thể
  • Sử dụng early return để tránh xử lý không cần thiết

2. Security Considerations:

// Ví dụ: Security headers middleware
export function middleware(request: NextRequest) {
  const response = NextResponse.next()
  
  // Thêm security headers
  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')
  
  return response
}

3. Error Handling:

export function middleware(request: NextRequest) {
  try {
    // Logic xử lý
    return NextResponse.next()
  } catch (error) {
    console.error('Middleware error:', error)
    // Trong production, redirect về error page
    return NextResponse.redirect(new URL('/error', request.url))
  }
}

4.2 So sánh Middleware vs Edge Functions vs Serverless Functions

Tính năngMiddlewareEdge FunctionsServerless Functions
Vị trí chạyTrước khi request đến appTại edge locationsTại server regions
Thời gian chạy< 1.5s< 30s< 300s
Memory limit128MB128MB1GB+
Node.js APIs
Database access❌ (trừ edge-compatible)❌ (trừ edge-compatible)
Use caseAuth, redirects, rewritesLightweight APIs, cachingHeavy computation, DB operations

4.3 Common Patterns và Anti-patterns

✅ Nên làm:

  • Authentication và authorization
  • URL redirects và rewrites
  • A/B testing và feature flags
  • Rate limiting cơ bản
  • Header manipulation
  • Simple data transformation

❌ Không nên làm:

  • Database queries phức tạp
  • File system operations
  • Heavy computations
  • Long-running processes
  • Complex business logic

4.4 Debugging và Testing

1. Local Development:

// middleware.ts
export function middleware(request: NextRequest) {
  // Thêm logging trong development
  if (process.env.NODE_ENV === 'development') {
    console.log('[Middleware]', {
      url: request.url,
      pathname: request.nextUrl.pathname,
      method: request.method,
      headers: Object.fromEntries(request.headers.entries())
    })
  }
  
  // Logic chính...
}

2. Testing với Jest:

// middleware.test.ts
import { NextRequest } from 'next/server'
import { middleware } from './middleware'

describe('Middleware', () => {
  it('should redirect to login when not authenticated', () => {
    const request = new NextRequest(new URL('http://localhost:3000/dashboard'))
    const response = middleware(request)
    
    expect(response.status).toBe(307)
    expect(response.headers.get('location')).toBe('http://localhost:3000/login')
  })
})

5. Bài tập nhanh

Bài tập 1: Multi-role Authorization System

Tạo middleware hỗ trợ nhiều role (admin, moderator, user) với permissions khác nhau:

// Yêu cầu:
// - Admin: Truy cập được tất cả routes
// - Moderator: Chỉ được /dashboard và /api/content/*
// - User: Chỉ được /dashboard và /profile/*
// - Guest: Chỉ được / và /login

// Gợi ý cấu trúc:
interface RolePermissions {
  [role: string]: string[]
}

const permissions: RolePermissions = {
  admin: ['*'],
  moderator: ['/dashboard', '/api/content/*'],
  user: ['/dashboard', '/profile/*'],
  guest: ['/', '/login']
}

Bài tập 2: Geographic Content Restriction

Tạo middleware giới hạn nội dung theo quốc gia:

// Yêu cầu:
// - Chặn access từ certain countries
// - Redirect users từ Châu Âu sang /eu-version
// - Log geographic data cho analytics
// - Support whitelist/blacklist IPs

// Gợi ý:
// Dùng request.geo.country và request.geo.region
// Tạo danh sách blocked countries
// Implement IP validation

Bài tập 3: Advanced Rate Limiting với Edge Functions

Tạo Edge Function rate limiting nâng cao:

// Yêu cầu:
// - Different limits cho different endpoints (/api/* vs static assets)
// - Support burst requests (cho phép vượt limit trong thời gian ngắn)
// - Implement sliding window thay vì fixed window
// - Return proper headers (X-RateLimit-Limit, X-RateLimit-Remaining)
// - Support IP whitelisting cho trusted sources

// Gợi ý cấu trúc:
interface RateLimitConfig {
  windowMs: number
  maxRequests: number
  burstLimit?: number
  skipSuccessfulRequests?: boolean
}

6. Kết luận

Middleware và Edge Functions là những công cụ cực kỳ mạnh mẽ trong Next.js, cho phép bạn:

Key Takeaways:

  1. Tốc độ vượt trội: Thực thi tại edge locations gần người dùng
  2. Khả năng mở rộng: Tự động scale theo traffic
  3. Chi phí hiệu quả: Tối ưu cho các tác vụ nhẹ
  4. Security tăng cường: Xử lý auth và validation sớm

Khi nào nên dùng:

  • Authentication và authorization đơn giản
  • URL redirects và rewrites
  • A/B testing và feature flags
  • Rate limiting và throttling
  • Geographic-based routing
  • Header manipulation

Khi không nên dùng:

  • Database operations phức tạp
  • Heavy computations
  • File system access
  • Long-running processes

Best Practices:

  • Luôn test kỹ trước khi deploy
  • Monitor performance và error rates
  • Sử dụng proper error handling
  • Implement logging cho debugging
  • Follow security best practices

Middleware và Edge Functions sẽ giúp bạn xây dựng các ứng dụng Next.js có hiệu suất cao, bảo mật tốt và khả năng mở rộng tuyệt vời. Hãy bắt đầu với các use case đơn giản và dần dần mở rộng theo nhu cầu của dự án.

18 bài học
Bài 15
Tiến độ hoàn thành 83%

Đã hoàn thành 15/18 bài học