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
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,cryptocủ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
edgeruntime vớigetStaticPropshoặcgetServerSideProps - 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ăng | Middleware | Edge Functions | Serverless Functions |
|---|---|---|---|
| Vị trí chạy | Trước khi request đến app | Tại edge locations | Tại server regions |
| Thời gian chạy | < 1.5s | < 30s | < 300s |
| Memory limit | 128MB | 128MB | 1GB+ |
| Node.js APIs | ❌ | ❌ | ✅ |
| Database access | ❌ (trừ edge-compatible) | ❌ (trừ edge-compatible) | ✅ |
| Use case | Auth, redirects, rewrites | Lightweight APIs, caching | Heavy 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:
- Tốc độ vượt trội: Thực thi tại edge locations gần người dùng
- Khả năng mở rộng: Tự động scale theo traffic
- Chi phí hiệu quả: Tối ưu cho các tác vụ nhẹ
- 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.