Next.js Middleware ขั้นสูง: จาก middleware.ts สู่ proxy.ts ใน Next.js 16 [คู่มือฉบับสมบูรณ์]

คู่มือ Next.js Middleware ฉบับสมบูรณ์ ครอบคลุม authentication, rate limiting, security headers, i18n และการย้ายไปใช้ proxy.ts ใน Next.js 16 พร้อมตัวอย่างโค้ดจริง

บทนำ: ทำไม Middleware ถึงสำคัญใน Next.js

ถ้าคุณเคยทำงานกับ Next.js App Router มาสักพัก คุณน่าจะรู้ดีว่า Middleware เป็นจุดยุทธศาสตร์ที่สำคัญมากจริงๆ ของแอปพลิเคชัน มันทำงานก่อนที่คำขอ (request) จะถูกส่งต่อไปยัง route ใดๆ ก็ตาม ไม่ว่าจะเป็นเรื่องการตรวจสอบสิทธิ์ (authentication) การจำกัดอัตราการเข้าถึง (rate limiting) หรือแม้แต่การจัดการภาษา — ทั้งหมดนี้ทำได้ในจุดเดียว

ซึ่งถ้าพูดตรงๆ ปี 2026 นี้ ภูมิทัศน์ของ Next.js Middleware เปลี่ยนไปเยอะมาก

ด้วยการเปิดตัว Next.js 16 ทีมพัฒนาได้เปลี่ยนชื่อจาก middleware.ts เป็น proxy.ts แถมยังรองรับ Node.js runtime แทนที่จะจำกัดอยู่แค่ Edge runtime เท่านั้น นี่เป็นการเปลี่ยนแปลงครั้งใหญ่ที่นักพัฒนาทุกคนควรรู้ไว้ เพราะมันเปิดความเป็นไปได้ใหม่ๆ เยอะเลย

งั้นเรามาเจาะลึกทุกแง่มุมของ Next.js Middleware กันเลย ตั้งแต่พื้นฐานไปจนถึงรูปแบบขั้นสูง รวมถึงการย้ายไปใช้ proxy.ts ใน Next.js 16 ด้วย

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

Middleware ใน Next.js คือฟังก์ชันที่ทำงานบน server ก่อนที่คำขอจะถูก route ไปยังหน้าเว็บหรือ API route ใดๆ ถ้าคุณเคยใช้ Express.js มา อาจจะคิดว่าคล้ายๆ กัน — แต่จริงๆ แล้วแตกต่างกันตรงที่ Next.js Middleware ทำงานที่ระดับ network boundary ของแอปพลิเคชันเลย

โครงสร้างพื้นฐานของ middleware.ts

// middleware.ts (Next.js 15)
import { NextResponse } from "next/server";
import type { NextRequest } from "next/server";

export function middleware(request: NextRequest) {
  // ตรวจสอบคำขอก่อนที่จะถูกส่งต่อ
  const url = request.nextUrl;

  // ตัวอย่าง: redirect ผู้ใช้ที่ยังไม่ล็อกอิน
  const token = request.cookies.get("auth-token");
  if (!token && url.pathname.startsWith("/dashboard")) {
    return NextResponse.redirect(new URL("/login", request.url));
  }

  return NextResponse.next();
}

// กำหนดว่า middleware จะทำงานกับ route ไหนบ้าง
export const config = {
  matcher: ["/dashboard/:path*", "/api/:path*"],
};

โค้ดข้างบนนี้เป็นรูปแบบพื้นฐานสุดๆ เลย แค่เช็ค token แล้วก็ redirect ถ้ายังไม่ได้ล็อกอิน ง่ายๆ แบบนี้แหละ

ลำดับการทำงาน (Execution Order)

สิ่งที่สำคัญมากคือคุณต้องเข้าใจลำดับการทำงานของ Middleware ให้ดี:

  1. Middleware ทำงานก่อน — ก่อนที่ Next.js จะ match route ใดๆ
  2. Route matching — Next.js หา route ที่ตรงกับ URL
  3. Layout/Page rendering — Server Components ถูก render
  4. Response ถูกส่งกลับ — ผู้ใช้ได้รับหน้าเว็บ

จะเห็นว่า Middleware อยู่ต้นสุดเลย มันจึงเป็นจุดที่ดีที่สุดในการดักจับและจัดการคำขอ

2. รูปแบบ Middleware ขั้นสูงสำหรับ Authentication

การตรวจสอบสิทธิ์เป็นหนึ่งในกรณีที่ใช้ Middleware บ่อยที่สุด พูดตรงๆ ว่าแทบทุกโปรเจกต์ที่ผมเจอมาจะมี auth middleware อยู่แล้ว แต่ในปี 2026 แนวทางที่ดีที่สุดคือใช้ "Defense-in-Depth" strategy ซึ่งหมายความว่าคุณไม่ควรพึ่งพา Middleware เพียงอย่างเดียวในการตรวจสอบสิทธิ์

รูปแบบ Defense-in-Depth

// middleware.ts
import { NextResponse } from "next/server";
import type { NextRequest } from "next/server";
import { verifyToken } from "@/lib/auth";

// เส้นทางที่ต้องการ authentication
const protectedRoutes = ["/dashboard", "/settings", "/profile"];
// เส้นทางที่เฉพาะ admin เท่านั้น
const adminRoutes = ["/admin"];

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

  // ตรวจสอบว่าเป็น protected route หรือไม่
  const isProtected = protectedRoutes.some((route) =>
    pathname.startsWith(route)
  );
  const isAdmin = adminRoutes.some((route) =>
    pathname.startsWith(route)
  );

  if (!isProtected && !isAdmin) {
    return NextResponse.next();
  }

  // ตรวจสอบ token จาก cookie
  const token = request.cookies.get("session-token")?.value;

  if (!token) {
    const loginUrl = new URL("/login", request.url);
    loginUrl.searchParams.set("callbackUrl", pathname);
    return NextResponse.redirect(loginUrl);
  }

  try {
    const payload = await verifyToken(token);

    // ตรวจสอบ role สำหรับ admin routes
    if (isAdmin && payload.role !== "admin") {
      return NextResponse.redirect(new URL("/unauthorized", request.url));
    }

    // เพิ่มข้อมูลผู้ใช้ลงใน headers เพื่อส่งต่อไปยัง route
    const response = NextResponse.next();
    response.headers.set("x-user-id", payload.userId);
    response.headers.set("x-user-role", payload.role);
    return response;
  } catch (error) {
    // Token ไม่ถูกต้อง ลบ cookie และ redirect
    const response = NextResponse.redirect(
      new URL("/login", request.url)
    );
    response.cookies.delete("session-token");
    return response;
  }
}

สังเกตว่าในโค้ดนี้เรามีการตั้ง callbackUrl ใน query string ด้วย เพื่อที่เมื่อผู้ใช้ล็อกอินแล้วจะได้กลับมาที่หน้าเดิมได้ สิ่งเล็กๆ แบบนี้สร้างความแตกต่างในเรื่อง UX ได้เยอะ

Data Access Layer (DAL) — ชั้นป้องกันที่สอง

นี่คือสิ่งที่หลายคนมักมองข้าม นอกจาก Middleware แล้ว คุณควรสร้าง Data Access Layer เพื่อตรวจสอบสิทธิ์ที่จุดเข้าถึงข้อมูลทุกจุดด้วย:

// lib/dal.ts
import { cookies } from "next/headers";
import { verifyToken } from "@/lib/auth";
import { redirect } from "next/navigation";
import { cache } from "react";

// ใช้ React cache เพื่อหลีกเลี่ยงการตรวจสอบซ้ำในรอบ render เดียวกัน
export const getAuthenticatedUser = cache(async () => {
  const cookieStore = await cookies();
  const token = cookieStore.get("session-token")?.value;

  if (!token) {
    redirect("/login");
  }

  try {
    const payload = await verifyToken(token);
    return payload;
  } catch {
    redirect("/login");
  }
});

// ฟังก์ชันสำหรับดึงข้อมูลผู้ใช้อย่างปลอดภัย
export async function getUserProfile(userId: string) {
  const currentUser = await getAuthenticatedUser();

  // ตรวจสอบว่าผู้ใช้มีสิทธิ์ดูข้อมูลนี้หรือไม่
  if (currentUser.userId !== userId && currentUser.role !== "admin") {
    throw new Error("Unauthorized");
  }

  // ดึงข้อมูลจาก database
  return db.users.findUnique({ where: { id: userId } });
}

การมี DAL แบบนี้ทำให้แม้ว่า Middleware จะพลาดไป (ซึ่งอาจเกิดขึ้นได้จากหลายสาเหตุ) ข้อมูลของผู้ใช้ก็ยังคงปลอดภัยอยู่

3. Rate Limiting ด้วย Middleware

Rate limiting เป็นสิ่งที่หลายคนชอบลืม จนกว่าจะโดน brute-force attack แล้วค่อยมานึกได้ (เคยเจอมาแล้ว ไม่สนุกเลย) ด้วย Middleware คุณสามารถจัดการเรื่องนี้ได้ก่อนที่คำขอจะถึง application code เสียอีก

Rate Limiting ด้วย Sliding Window Algorithm

// lib/rate-limit.ts
interface RateLimitEntry {
  count: number;
  resetAt: number;
}

// สำหรับ single-instance ใช้ Map ธรรมดา
// สำหรับ production ควรใช้ Redis/Upstash
const rateLimitMap = new Map<string, RateLimitEntry>();

export function rateLimit(
  identifier: string,
  limit: number = 60,
  windowMs: number = 60_000
): { success: boolean; remaining: number; resetAt: number } {
  const now = Date.now();
  const entry = rateLimitMap.get(identifier);

  if (!entry || now > entry.resetAt) {
    const resetAt = now + windowMs;
    rateLimitMap.set(identifier, { count: 1, resetAt });
    return { success: true, remaining: limit - 1, resetAt };
  }

  if (entry.count >= limit) {
    return { success: false, remaining: 0, resetAt: entry.resetAt };
  }

  entry.count++;
  return {
    success: true,
    remaining: limit - entry.count,
    resetAt: entry.resetAt,
  };
}

การนำไปใช้ใน Middleware

// middleware.ts
import { NextResponse } from "next/server";
import type { NextRequest } from "next/server";
import { rateLimit } from "@/lib/rate-limit";

export function middleware(request: NextRequest) {
  // ใช้ IP address เป็น identifier
  const ip =
    request.headers.get("x-forwarded-for")?.split(",")[0] ??
    request.headers.get("x-real-ip") ??
    "unknown";

  // กำหนด limit ที่แตกต่างกันสำหรับ API routes
  const isApiRoute = request.nextUrl.pathname.startsWith("/api");
  const limit = isApiRoute ? 30 : 100; // 30 req/min สำหรับ API, 100 สำหรับหน้าเว็บ

  const { success, remaining, resetAt } = rateLimit(
    `${ip}:${isApiRoute ? "api" : "page"}`,
    limit
  );

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

  const response = NextResponse.next();
  response.headers.set("X-RateLimit-Limit", String(limit));
  response.headers.set("X-RateLimit-Remaining", String(remaining));
  response.headers.set("X-RateLimit-Reset", String(resetAt));

  return response;
}

จุดที่น่าสังเกตคือเราแยก limit ระหว่าง API routes (30 req/min) กับหน้าเว็บปกติ (100 req/min) ซึ่งก็สมเหตุสมผล เพราะ API จะถูกยิงบ่อยกว่าหน้าเว็บธรรมดา

Rate Limiting ด้วย Upstash สำหรับ Production

ต้องเตือนไว้ก่อนว่า ถ้าแอปของคุณมีหลาย instance (ซึ่งในปี 2026 แทบทุกแอปก็เป็น) การใช้ in-memory Map อย่างเดียวไม่พอ คุณต้องใช้ Redis-based rate limiting อย่าง Upstash:

// middleware.ts (production)
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(),
  // อนุญาต 60 requests ต่อ 60 วินาที
  limiter: Ratelimit.slidingWindow(60, "60 s"),
  analytics: true,
  prefix: "nextjs-middleware",
});

export async function middleware(request: NextRequest) {
  const ip = request.headers.get("x-forwarded-for") ?? "127.0.0.1";

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

  if (!success) {
    return NextResponse.json(
      { error: "Rate limit exceeded" },
      {
        status: 429,
        headers: {
          "X-RateLimit-Limit": String(limit),
          "X-RateLimit-Remaining": String(remaining),
          "X-RateLimit-Reset": String(reset),
        },
      }
    );
  }

  return NextResponse.next();
}

4. Security Headers และ CORS

เรื่อง security headers เนี่ย ตรงไปตรงมาเลย — Middleware เป็นจุดที่เหมาะที่สุดในการเพิ่ม security headers ให้กับทุกคำขอ เพราะจัดการที่เดียวได้หมด ไม่ต้องไปกระจายตาม route ต่างๆ ให้วุ่นวาย

การเพิ่ม Security Headers

// lib/security-headers.ts
export const securityHeaders = {
  // ป้องกัน clickjacking
  "X-Frame-Options": "DENY",
  // ป้องกัน MIME type sniffing
  "X-Content-Type-Options": "nosniff",
  // ป้องกัน XSS
  "X-XSS-Protection": "1; mode=block",
  // ควบคุม referrer information
  "Referrer-Policy": "strict-origin-when-cross-origin",
  // ป้องกันการเข้าถึง browser features
  "Permissions-Policy":
    "camera=(), microphone=(), geolocation=(), interest-cohort=()",
  // บังคับใช้ HTTPS
  "Strict-Transport-Security":
    "max-age=31536000; includeSubDomains; preload",
  // Content Security Policy
  "Content-Security-Policy":
    "default-src 'self'; script-src 'self' 'unsafe-inline' 'unsafe-eval'; style-src 'self' 'unsafe-inline'; img-src 'self' data: https:; font-src 'self' data:;",
};
// middleware.ts
import { NextResponse } from "next/server";
import type { NextRequest } from "next/server";
import { securityHeaders } from "@/lib/security-headers";

export function middleware(request: NextRequest) {
  const response = NextResponse.next();

  // เพิ่ม security headers ให้กับทุก response
  Object.entries(securityHeaders).forEach(([key, value]) => {
    response.headers.set(key, value);
  });

  return response;
}

การจัดการ CORS สำหรับ API Routes

ถ้าคุณมี API ที่ต้องรับ request จากโดเมนอื่น CORS เป็นเรื่องที่หนีไม่พ้น ลองดูตัวอย่างนี้:

// middleware.ts
const allowedOrigins = [
  "https://myapp.com",
  "https://staging.myapp.com",
];

export function middleware(request: NextRequest) {
  const origin = request.headers.get("origin");
  const isApiRoute = request.nextUrl.pathname.startsWith("/api");

  // จัดการ CORS สำหรับ API routes เท่านั้น
  if (isApiRoute) {
    // จัดการ preflight requests
    if (request.method === "OPTIONS") {
      const response = new NextResponse(null, { status: 204 });

      if (origin && allowedOrigins.includes(origin)) {
        response.headers.set(
          "Access-Control-Allow-Origin", origin
        );
        response.headers.set(
          "Access-Control-Allow-Methods",
          "GET, POST, PUT, DELETE, OPTIONS"
        );
        response.headers.set(
          "Access-Control-Allow-Headers",
          "Content-Type, Authorization"
        );
        response.headers.set(
          "Access-Control-Max-Age", "86400"
        );
      }

      return response;
    }

    const response = NextResponse.next();
    if (origin && allowedOrigins.includes(origin)) {
      response.headers.set(
        "Access-Control-Allow-Origin", origin
      );
    }

    return response;
  }

  return NextResponse.next();
}

อย่าลืมว่าเราเช็ค origin กับ whitelist เสมอนะ อย่าใช้ * ใน production เด็ดขาด มันเปิดช่องโหว่ได้ง่ายมาก

5. Internationalization (i18n) ด้วย Middleware

ถ้าแอปของคุณรองรับหลายภาษา Middleware เป็นจุดที่เหมาะมากในการจัดการเรื่องนี้ เพราะสามารถตรวจจับภาษาของผู้ใช้แล้ว redirect ไปยัง locale ที่เหมาะสมก่อนที่จะ render หน้าเว็บเสียอีก

การจัดการ Locale Detection

// middleware.ts
import { NextResponse } from "next/server";
import type { NextRequest } from "next/server";
import { match } from "@formatjs/intl-localematcher";
import Negotiator from "negotiator";

const locales = ["th", "en", "ja", "zh"];
const defaultLocale = "th";

function getPreferredLocale(request: NextRequest): string {
  // ตรวจสอบ cookie ก่อน
  const localeCookie = request.cookies.get("preferred-locale");
  if (localeCookie && locales.includes(localeCookie.value)) {
    return localeCookie.value;
  }

  // ใช้ Accept-Language header
  const headers: Record<string, string> = {};
  request.headers.forEach((value, key) => {
    headers[key] = value;
  });

  const languages = new Negotiator({ headers }).languages();

  try {
    return match(languages, locales, defaultLocale);
  } catch {
    return defaultLocale;
  }
}

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

  // ตรวจสอบว่า pathname มี locale อยู่แล้วหรือไม่
  const hasLocale = locales.some(
    (locale) =>
      pathname.startsWith(`/${locale}/`) ||
      pathname === `/${locale}`
  );

  if (hasLocale) {
    return NextResponse.next();
  }

  // ข้าม static files และ API routes
  if (
    pathname.startsWith("/_next") ||
    pathname.startsWith("/api") ||
    pathname.includes(".")
  ) {
    return NextResponse.next();
  }

  // ตรวจจับ locale และ redirect
  const locale = getPreferredLocale(request);
  const newUrl = new URL(`/${locale}${pathname}`, request.url);
  return NextResponse.redirect(newUrl);
}

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

ลำดับการตรวจจับภาษาตรงนี้ค่อนข้างสำคัญ — เราเช็ค cookie ก่อน (เผื่อผู้ใช้เคยเลือกภาษาไว้แล้ว) แล้วค่อย fallback ไปดู Accept-Language header เป็น logic ที่ดูเรียบง่ายแต่ครอบคลุมกรณีส่วนใหญ่ได้ดี

6. การรวม Middleware Patterns เข้าด้วยกัน

ทีนี้มาถึงปัญหาคลาสสิกแล้ว — Next.js อนุญาตให้มีไฟล์ middleware.ts เพียงไฟล์เดียวต่อโปรเจกต์ แล้วจะทำยังไงดี?

คำตอบคือแยกเป็น modular functions แล้วรวมกันด้วย Chain of Responsibility pattern:

// middleware/auth.ts
import type { NextRequest } from "next/server";
import { NextResponse } from "next/server";

export async function authMiddleware(
  request: NextRequest
): Promise<NextResponse | null> {
  const protectedPaths = ["/dashboard", "/settings"];
  const { pathname } = request.nextUrl;

  const isProtected = protectedPaths.some((path) =>
    pathname.startsWith(path)
  );

  if (!isProtected) return null;

  const token = request.cookies.get("session-token")?.value;
  if (!token) {
    return NextResponse.redirect(new URL("/login", request.url));
  }

  return null; // ส่งต่อไปยัง middleware ถัดไป
}
// middleware/rate-limit.ts
import type { NextRequest } from "next/server";
import { NextResponse } from "next/server";
import { rateLimit } from "@/lib/rate-limit";

export function rateLimitMiddleware(
  request: NextRequest
): NextResponse | null {
  const ip = request.headers.get("x-forwarded-for") ?? "unknown";
  const { success } = rateLimit(ip, 100);

  if (!success) {
    return NextResponse.json(
      { error: "Too many requests" },
      { status: 429 }
    );
  }

  return null;
}
// middleware.ts — ไฟล์หลักที่รวมทุกอย่างเข้าด้วยกัน
import { NextResponse } from "next/server";
import type { NextRequest } from "next/server";
import { authMiddleware } from "@/middleware/auth";
import { rateLimitMiddleware } from "@/middleware/rate-limit";
import { securityHeaders } from "@/lib/security-headers";

type MiddlewareFunction = (
  request: NextRequest
) => Promise<NextResponse | null> | NextResponse | null;

// Chain of Responsibility pattern
async function runMiddlewareChain(
  request: NextRequest,
  middlewares: MiddlewareFunction[]
): Promise<NextResponse> {
  for (const mw of middlewares) {
    const result = await mw(request);
    if (result) return result; // ถ้า middleware ใดส่ง response กลับ ให้หยุด
  }
  return NextResponse.next();
}

export async function middleware(request: NextRequest) {
  // เรียก middleware chain ตามลำดับ
  const response = await runMiddlewareChain(request, [
    rateLimitMiddleware,
    authMiddleware,
  ]);

  // เพิ่ม security headers ให้กับทุก response
  Object.entries(securityHeaders).forEach(([key, value]) => {
    response.headers.set(key, value);
  });

  return response;
}

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

วิธีนี้สวยงามมาก เพราะ:

  • แยก concerns ออกเป็นไฟล์ต่างๆ ได้อย่างชัดเจน
  • เพิ่มหรือลบ middleware ได้ง่ายแค่เพิ่ม/ลบจาก array
  • ทดสอบแต่ละ middleware แยกกันได้สบาย
  • ควบคุมลำดับการทำงานได้ เช่น ให้ rate limiting ทำงานก่อน authentication (เพราะไม่มีประโยชน์ที่จะ verify token ถ้า rate limit เกินแล้ว)

7. การย้ายจาก middleware.ts ไปเป็น proxy.ts (Next.js 16)

มาถึงส่วนที่หลายคนรอคอย — การเปลี่ยนแปลงใน Next.js 16

พูดตรงๆ ว่าตอนแรกที่ได้ยินว่าจะเปลี่ยนชื่อจาก middleware.ts เป็น proxy.ts ผมก็สงสัยว่าทำไมต้องเปลี่ยน แต่พอลองใช้จริงก็เข้าใจเหตุผลมากขึ้น

ทำไมถึงเปลี่ยนชื่อ?

  • ลดความสับสน: คำว่า "middleware" มักถูกสับสนกับ Express.js middleware ซึ่งมีแนวคิดที่แตกต่างกัน
  • สื่อความหมายชัดเจน: "proxy" บอกตรงๆ ว่านี่คือจุดที่อยู่ระหว่าง client กับ application
  • Node.js runtime: proxy.ts ทำงานบน Node.js runtime ซึ่งรองรับ API ที่กว้างกว่า Edge runtime มาก
  • แนะนำให้ใช้เป็น last resort: ทีม Next.js ต้องการสื่อว่าฟีเจอร์นี้ควรใช้เป็นทางเลือกสุดท้าย ไม่ใช่เป็นจุดแรกที่คุณนึกถึง

การ Migrate ด้วย Codemod

ข่าวดีคือ Next.js มี codemod ให้ใช้ ไม่ต้องมานั่งแก้มือ:

# ใช้ codemod เพื่อ migrate อัตโนมัติ
npx @next/codemod@latest middleware-to-proxy .

Codemod จะทำสิ่งเหล่านี้ให้:

  1. เปลี่ยนชื่อไฟล์จาก middleware.ts เป็น proxy.ts
  2. เปลี่ยนชื่อ exported function จาก middleware เป็น proxy

แค่นี้จริงๆ ส่วน logic ข้างในไม่ต้องแก้อะไร ค่อนข้างราบรื่น

ตัวอย่าง proxy.ts

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

// ตอนนี้ทำงานบน Node.js runtime!
// สามารถใช้ Node.js APIs ได้เต็มที่
export function proxy(request: NextRequest) {
  const url = request.nextUrl;

  // ตัวอย่าง: redirect ไปยัง locale ที่เหมาะสม
  if (url.pathname === "/") {
    return NextResponse.redirect(new URL("/th", request.url));
  }

  return NextResponse.next();
}

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

ข้อดีของ Node.js Runtime ใน proxy.ts

นี่แหละคือเหตุผลหลักที่ทำให้การเปลี่ยนแปลงนี้น่าตื่นเต้นมาก:

  • เชื่อมต่อ database โดยตรง: สามารถ query database เพื่อ validate session ได้เลย ซึ่งก่อนหน้านี้ทำไม่ได้บน Edge runtime (เป็นข้อจำกัดที่น่าหงุดหงิดมาก)
  • ใช้ Node.js modules: ใช้ libraries ที่ต้องการ Node.js APIs ได้หมดเลย
  • ลดข้อจำกัด: ไม่ถูกจำกัดด้วยขนาดของ Edge function อีกต่อไป
// proxy.ts — ตัวอย่างการใช้ Node.js APIs
import { NextResponse } from "next/server";
import type { NextRequest } from "next/server";
import { db } from "@/lib/database"; // เชื่อมต่อ DB ได้โดยตรง!

export async function proxy(request: NextRequest) {
  const sessionId = request.cookies.get("session-id")?.value;

  if (sessionId) {
    // ตรวจสอบ session กับ database โดยตรง
    const session = await db.sessions.findUnique({
      where: { id: sessionId },
      include: { user: true },
    });

    if (session && session.expiresAt > new Date()) {
      const response = NextResponse.next();
      response.headers.set("x-user-id", session.user.id);
      return response;
    }
  }

  // ไม่มี session ที่ valid
  if (request.nextUrl.pathname.startsWith("/dashboard")) {
    return NextResponse.redirect(new URL("/login", request.url));
  }

  return NextResponse.next();
}

ลองนึกดู แค่เปลี่ยนจาก Edge เป็น Node.js runtime ก็ทำให้ query database ได้ตรงๆ เลย ไม่ต้องอ้อมไปเรียก API endpoint อีกทีแบบเดิมอีกต่อไป

8. Matcher Configuration ที่มีประสิทธิภาพ

เรื่อง matcher ดูเหมือนเล็กน้อย แต่จริงๆ แล้วสำคัญมาก เพราะ matcher ที่ดีจะช่วยลดจำนวนคำขอที่ต้องผ่าน Middleware ลงอย่างมาก ส่งผลโดยตรงต่อ performance ของแอป

รูปแบบ Matcher ที่ใช้บ่อย

export const config = {
  matcher: [
    // จับคู่ทุก route ยกเว้น static files
    "/((?!_next/static|_next/image|favicon.ico).*)",

    // จับคู่เฉพาะ API routes
    "/api/:path*",

    // จับคู่เฉพาะ routes ที่ต้องการ auth
    "/dashboard/:path*",
    "/settings/:path*",

    // ใช้ regex pattern
    "/(th|en|ja)/:path*",
  ],
};

เคล็ดลับสำหรับ Matcher

  • ใช้ matcher ที่เฉพาะเจาะจงที่สุดเท่าที่จะทำได้ เพื่อลด overhead
  • หลีกเลี่ยงการจับคู่ static files อย่าง _next/static, _next/image หรือไฟล์ที่มี extension
  • ถ้า API routes กับ page routes ใช้ logic ต่างกัน ก็ควรแยก matcher ออกจากกัน
  • ทดสอบ matcher patterns กับ URL ที่หลากหลาย อย่าลืมเคส edge case ด้วย

9. การจัดการ Error ใน Middleware

ส่วนนี้สำคัญมากจริงๆ ถ้า Middleware ล้มเหลว ผู้ใช้จะเข้าถึงหน้าเว็บไม่ได้เลย เป็นเรื่องที่ร้ายแรงมากสำหรับ production

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

export async function middleware(request: NextRequest) {
  try {
    // Logic ของ middleware ทั้งหมดอยู่ใน try block
    const token = request.cookies.get("session-token")?.value;

    if (token) {
      const isValid = await validateToken(token);
      if (!isValid) {
        // Token ไม่ถูกต้อง — ลบ cookie
        const response = NextResponse.redirect(
          new URL("/login", request.url)
        );
        response.cookies.delete("session-token");
        return response;
      }
    }

    return NextResponse.next();
  } catch (error) {
    // สำคัญ: ถ้า middleware error ให้ปล่อยคำขอผ่านไป
    // แทนที่จะ block ผู้ใช้ทั้งหมด
    console.error("Middleware error:", error);

    // Fallback: ปล่อยคำขอผ่านไป
    // ชั้นป้องกันอื่นๆ (DAL) จะจัดการ auth ต่อไป
    return NextResponse.next();
  }
}

หลักการตรงนี้คือ "fail open" — ถ้า middleware error ให้ปล่อยคำขอผ่านไปก่อน แล้วพึ่งชั้นป้องกันอื่นๆ เช่น DAL ที่เราทำไว้ในหัวข้อก่อนหน้า

สรุปหลักการจัดการ error สั้นๆ :

  • อย่า block ผู้ใช้ทั้งหมด: ปล่อยให้คำขอผ่านไป (fail open) ดีกว่าให้ทั้งเว็บใช้งานไม่ได้
  • Log errors ให้ดี: บันทึกเพื่อ debug แต่อย่า expose ข้อมูลภายในให้ผู้ใช้เห็น
  • มี fallback เสมอ: Data Access Layer คือเพื่อนที่ดีที่สุดของคุณ

10. Best Practices และสิ่งที่ควรหลีกเลี่ยง

สิ่งที่ควรทำ

  • ทำให้ middleware เบาและเร็ว: มันทำงานกับทุกคำขอ ดังนั้นหลีกเลี่ยงงานหนักๆ ให้มากที่สุด
  • ใช้ matcher เฉพาะเจาะจง: จำกัดให้ทำงานเฉพาะ routes ที่จำเป็นจริงๆ
  • แยก logic เป็น modules: อย่ายัดทุกอย่างลงไฟล์เดียว มันดูแลยากมาก
  • ใช้ Defense-in-Depth: พึ่งพาหลายชั้นการป้องกัน ไม่ใช่แค่ middleware อย่างเดียว
  • เขียน tests: Middleware สำคัญขนาดนี้ ต้องมี unit tests ด้วย

สิ่งที่ควรหลีกเลี่ยง

  • อย่าทำงานหนักใน middleware: หลีกเลี่ยง database query ใน Edge runtime (แต่ใน proxy.ts ของ Next.js 16 ทำได้แล้วนะ)
  • อย่าเก็บ state ใน middleware: มันไม่มี persistent memory ถ้าต้องการเก็บ state ใช้ Redis หรือ external storage
  • ระวัง redirect loop: นี่เป็นบั๊กที่เจอบ่อยมาก ลองตรวจสอบ logic ให้ดีก่อน deploy
  • อย่าลืม matcher: ถ้าไม่กำหนด middleware จะทำงานกับ ทุก คำขอ รวมถึง static files ด้วย
  • อย่าใช้ Node.js-only modules ใน Edge runtime: ตรวจสอบ compatibility ก่อนเสมอ

11. ตัวอย่างจริง: E-commerce Middleware

มาดูตัวอย่างที่รวมทุกรูปแบบที่เราเรียนมาทั้งหมดเข้าด้วยกัน สมมติเราทำแอป e-commerce:

// proxy.ts (Next.js 16)
import { NextResponse } from "next/server";
import type { NextRequest } from "next/server";
import { db } from "@/lib/database";
import { rateLimit } from "@/lib/rate-limit";
import { securityHeaders } from "@/lib/security-headers";

const locales = ["th", "en"];
const defaultLocale = "th";

export async function proxy(request: NextRequest) {
  const { pathname } = request.nextUrl;
  const ip = request.headers.get("x-forwarded-for") ?? "unknown";

  // 1. Rate limiting
  const isApi = pathname.startsWith("/api");
  const { success } = rateLimit(ip, isApi ? 30 : 100);

  if (!success) {
    return NextResponse.json(
      { error: "Too many requests" },
      { status: 429 }
    );
  }

  // 2. Authentication สำหรับ protected routes
  const protectedPaths = ["/checkout", "/orders", "/account"];
  const isProtected = protectedPaths.some((p) =>
    pathname.replace(/^\/(th|en)/, "").startsWith(p)
  );

  if (isProtected) {
    const sessionId = request.cookies.get("session-id")?.value;

    if (!sessionId) {
      return NextResponse.redirect(new URL("/login", request.url));
    }

    // ตรวจสอบ session กับ database (ทำได้ใน proxy.ts!)
    const session = await db.sessions.findUnique({
      where: { id: sessionId },
    });

    if (!session || session.expiresAt < new Date()) {
      const response = NextResponse.redirect(
        new URL("/login", request.url)
      );
      response.cookies.delete("session-id");
      return response;
    }
  }

  // 3. i18n locale detection
  const hasLocale = locales.some(
    (l) => pathname.startsWith(`/${l}/`) || pathname === `/${l}`
  );

  if (
    !hasLocale &&
    !pathname.startsWith("/_next") &&
    !pathname.startsWith("/api") &&
    !pathname.includes(".")
  ) {
    const preferred =
      request.cookies.get("locale")?.value ?? defaultLocale;
    return NextResponse.redirect(
      new URL(`/${preferred}${pathname}`, request.url)
    );
  }

  // 4. เพิ่ม security headers
  const response = NextResponse.next();
  Object.entries(securityHeaders).forEach(([key, value]) => {
    response.headers.set(key, value);
  });

  return response;
}

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

ตัวอย่างนี้รวม rate limiting, authentication (พร้อม database query ตรงๆ), i18n locale detection, และ security headers เข้าด้วยกันในไฟล์เดียว ซึ่งในชีวิตจริง คุณอาจจะแยกเป็น modules ตามแนวทางในหัวข้อที่ 6 เพื่อให้ดูแลรักษาง่ายกว่า

สรุป

ถ้าอ่านมาถึงตรงนี้แล้ว ต้องบอกว่าคุณจริงจังมาก (และขอบคุณที่อ่านจนจบ!)

Next.js Middleware — หรือจะเรียกว่า proxy.ts ในเวอร์ชัน 16 — เป็นเครื่องมือที่ทรงพลังจริงๆ สำหรับการจัดการคำขอก่อนที่จะถึง application code ในบทความนี้เราครอบคลุมไปเยอะทีเดียว:

  • Authentication patterns: การใช้ Defense-in-Depth strategy ร่วมกับ Data Access Layer
  • Rate limiting: ทั้งแบบ in-memory สำหรับ dev และ Redis-based สำหรับ production
  • Security headers และ CORS: เพิ่มชั้นป้องกันให้แอปแบบจัดการจุดเดียว
  • Internationalization: locale detection และ redirection อัตโนมัติ
  • Middleware composition: รวม patterns ต่างๆ ด้วย Chain of Responsibility
  • การ migrate ไป proxy.ts: ใช้ codemod แล้วก็เปลี่ยนได้เลย
  • Error handling: fail open แล้วพึ่ง DAL เป็นชั้นป้องกันถัดไป

การเปลี่ยนจาก middleware.ts ไปเป็น proxy.ts ใน Next.js 16 ถือว่าเป็นก้าวที่ดีมาก โดยเฉพาะเรื่อง Node.js runtime ที่ทำให้เชื่อมต่อ database ได้โดยตรง ไม่ว่าคุณจะยังอยู่กับ Next.js 15 หรือกำลังจะ upgrade ไป 16 รูปแบบและแนวทางที่เราพูดถึงในบทความนี้จะช่วยให้คุณสร้างแอปที่ปลอดภัย มีประสิทธิภาพ และดูแลรักษาง่ายขึ้นแน่นอน

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

Our team of expert writers and editors.