jvinhit//lab

Search posts

Type to search across journal entries.

navigate open esc close

Next.js 16 from Zero to Senior · Part 7 — Route Handlers, APIs & Proxy

Build HTTP endpoints with route.ts, work with the async cookies() and headers(), stream responses, pick Node vs Edge runtime, handle webhooks and CORS, and use proxy.ts — the renamed middleware in Next.js 16.

Server Actions (Part 6) cover most mutations from your own UI {Server Actions (Phần 6) lo phần lớn mutation từ UI của chính bạn}. But you still need real HTTP endpoints for webhooks, third-party callbacks, mobile clients, file downloads, and public APIs {Nhưng bạn vẫn cần endpoint HTTP thật cho webhook, callback bên thứ ba, client mobile, tải file, và API công khai}. That’s what Route Handlers are for {Đó là việc của Route Handlers}. We’ll also meet proxy.ts — what Next.js 16 renamed middleware to {Ta cũng sẽ gặp proxy.ts — cái mà Next.js 16 đổi tên middleware thành}.


1. route.ts — an endpoint, not a page {route.ts — một endpoint, không phải page}

A route.ts (or route.tsx) file exports functions named after HTTP methods {Một file route.ts export các hàm đặt tên theo HTTP method}. It produces a real API endpoint with no UI {Nó tạo một API endpoint thật không có UI}:

// app/api/health/route.ts  →  GET /api/health
export async function GET() {
  return Response.json({ status: 'ok', time: Date.now() });
}

You can export GET, POST, PUT, PATCH, DELETE, HEAD, and OPTIONS {Bạn có thể export GET, POST, PUT, PATCH, DELETE, HEAD, và OPTIONS}. They use the Web standard Request and Response objects — the same APIs you’d use in any modern runtime {Chúng dùng đối tượng RequestResponse theo chuẩn Web — cùng API bạn dùng ở bất kỳ runtime hiện đại nào}.

A folder can have either a page.tsx or a route.ts, not both — one serves UI, the other serves data {Một thư mục có hoặc page.tsx hoặc route.ts, không cả hai — một phục vụ UI, một phục vụ data}.


2. Reading input {Đọc input}

The request body {Body của request}

// app/api/todos/route.ts
import { z } from 'zod';

const Body = z.object({ title: z.string().min(1) });

export async function POST(request: Request) {
  const json = await request.json();
  const parsed = Body.safeParse(json);
  if (!parsed.success) {
    return Response.json({ error: 'Invalid body' }, { status: 400 });
  }
  const todo = await db.todo.create({ data: parsed.data });
  return Response.json(todo, { status: 201 });
}

Query params & dynamic segments {Query param & dynamic segment}

// app/api/search/route.ts  →  /api/search?q=next
export async function GET(request: Request) {
  const { searchParams } = new URL(request.url);
  const q = searchParams.get('q') ?? '';
  return Response.json(await search(q));
}
// app/api/users/[id]/route.ts  →  /api/users/42
export async function GET(
  request: Request,
  { params }: { params: Promise<{ id: string }> }
) {
  const { id } = await params; // params is async here too (Next.js 16)
  const user = await db.user.findUnique({ where: { id } });
  if (!user) return new Response('Not found', { status: 404 });
  return Response.json(user);
}

3. The async cookies() and headers() {cookies()headers() bất đồng bộ}

In Next.js 16 these request APIs are async — you must await them {Ở Next.js 16 các request API này là bất đồng bộ — bạn phải await chúng}:

import { cookies, headers } from 'next/headers';

export async function GET() {
  const cookieStore = await cookies();
  const token = cookieStore.get('session')?.value;

  const headerList = await headers();
  const ua = headerList.get('user-agent');

  return Response.json({ hasSession: Boolean(token), ua });
}

You can set cookies on the way out too {Bạn cũng có thể set cookie khi trả về}:

export async function POST() {
  const cookieStore = await cookies();
  cookieStore.set('session', token, {
    httpOnly: true,
    secure: true,
    sameSite: 'lax',
    path: '/',
    maxAge: 60 * 60 * 24 * 7,
  });
  return Response.json({ ok: true });
}

These same async APIs work inside Server Components and Server Actions — reading them makes a route dynamic {Cùng các API async này hoạt động trong Server Components và Server Actions — đọc chúng làm một route trở nên động}.


4. Streaming responses {Stream response}

Return a ReadableStream to stream data (AI token streams, large exports, server-sent events) {Trả về một ReadableStream để stream dữ liệu (luồng token AI, export lớn, server-sent events)}:

// app/api/stream/route.ts
export async function GET() {
  const encoder = new TextEncoder();
  const stream = new ReadableStream({
    async start(controller) {
      for (let i = 0; i < 5; i++) {
        controller.enqueue(encoder.encode(`chunk ${i}\n`));
        await new Promise((r) => setTimeout(r, 500));
      }
      controller.close();
    },
  });
  return new Response(stream, {
    headers: { 'Content-Type': 'text/plain; charset=utf-8' },
  });
}

5. Node vs Edge runtime {Runtime Node vs Edge}

Route handlers run on the Node.js runtime by default in Next.js 16 {Route handler chạy trên runtime Node.js theo mặc định ở Next.js 16}. Opt a route into the lighter Edge runtime when you need global low latency and don’t need Node APIs {Chọn route sang runtime Edge nhẹ hơn khi cần độ trễ thấp toàn cầu và không cần Node API}:

export const runtime = 'edge'; // or 'nodejs' (default)
Node.js (default)Edge
APIs {API}Full Node (fs, crypto, DB drivers)Web APIs only {Chỉ Web API}
Cold start {Khởi động lạnh}Larger {Lớn hơn}Tiny {Rất nhỏ}
Best for {Hợp cho}DB access, heavy logic {Truy cập DB, logic nặng}Geo-routing, simple transforms {Định tuyến địa lý, biến đổi đơn giản}

Pick Node unless you have a specific latency reason for Edge {Chọn Node trừ khi có lý do độ trễ cụ thể cho Edge}.


6. Caching a GET handler {Cache một GET handler}

Like everything in Next.js 16, route handlers are dynamic by default {Như mọi thứ ở Next.js 16, route handler là động theo mặc định}. To cache a GET, use the Cache Components primitives from Part 4 inside a cached data function {Để cache một GET, dùng primitive Cache Components từ Phần 4 trong một hàm dữ liệu đã cache}:

import { cacheLife, cacheTag } from 'next/cache';

async function getCatalog() {
  'use cache';
  cacheLife('hours');
  cacheTag('catalog');
  return db.product.findMany();
}

export async function GET() {
  return Response.json(await getCatalog()); // served from cache, invalidate via cacheTag
}

7. CORS for public APIs {CORS cho API công khai}

Set CORS headers explicitly and handle the OPTIONS preflight {Set header CORS rõ ràng và xử lý preflight OPTIONS}:

const cors = {
  'Access-Control-Allow-Origin': 'https://app.example.com',
  'Access-Control-Allow-Methods': 'GET, POST, OPTIONS',
  'Access-Control-Allow-Headers': 'Content-Type, Authorization',
};

export async function OPTIONS() {
  return new Response(null, { status: 204, headers: cors });
}

export async function GET() {
  return Response.json({ ok: true }, { headers: cors });
}

Avoid Access-Control-Allow-Origin: * for anything authenticated — pin it to known origins {Tránh Access-Control-Allow-Origin: * cho bất cứ gì cần xác thực — ghim vào origin đã biết}.


8. Webhooks: verify the signature {Webhook: xác minh chữ ký}

Webhooks (Stripe, GitHub…) need the raw body to verify the signature — read it as text, not JSON {Webhook (Stripe, GitHub…) cần body thô để xác minh chữ ký — đọc dưới dạng text, không phải JSON}:

// app/api/webhooks/stripe/route.ts
import { headers } from 'next/headers';

export async function POST(request: Request) {
  const body = await request.text();                 // raw body for signing
  const sig = (await headers()).get('stripe-signature');
  const event = verifyStripe(body, sig, process.env.STRIPE_WEBHOOK_SECRET!);
  // ...handle event.type
  return Response.json({ received: true });
}

9. proxy.ts — the renamed middleware {proxy.ts — middleware đổi tên}

This is a major Next.js 16 change {Đây là thay đổi lớn ở Next.js 16}: middleware.ts is renamed to proxy.ts, clarifying that its job is the network boundary — redirecting, rewriting, and gating requests before they reach a route {middleware.ts đổi tên thành proxy.ts, làm rõ rằng việc của nó là ranh giới mạng — chuyển hướng, rewrite, và chặn request trước khi chúng tới một route}. It also now runs on the Node.js runtime by default (it was Edge-only before) {Nó cũng chạy trên runtime Node.js theo mặc định (trước đây chỉ Edge)}.

// proxy.ts  (at the project root, or src/)
import { NextResponse, type NextRequest } from 'next/server';

export function proxy(request: NextRequest) {
  const isLoggedIn = Boolean(request.cookies.get('session'));
  const isProtected = request.nextUrl.pathname.startsWith('/dashboard');

  if (isProtected && !isLoggedIn) {
    return NextResponse.redirect(new URL('/login', request.url));
  }
  return NextResponse.next();
}

export const config = {
  matcher: ['/dashboard/:path*'], // only run on these paths
};

Use proxy.ts for fast, coarse gating — auth redirects, locale detection, A/B rewrites, bot blocking {Dùng proxy.ts cho chặn nhanh, thô — chuyển hướng auth, phát hiện locale, rewrite A/B, chặn bot}. Do not put heavy logic or DB queries in it; it runs on every matched request {Đừng đặt logic nặng hay truy vấn DB vào đó; nó chạy trên mọi request khớp}. Real authorization still belongs in your Data Access Layer (Part 9) {Phân quyền thật vẫn thuộc về Data Access Layer (Phần 9)}.

Migrating from 15? Rename middleware.tsproxy.ts and the exported middleware function → proxy. The config.matcher API is unchanged {Di trú từ 15? Đổi tên middleware.tsproxy.ts và hàm export middlewareproxy. API config.matcher không đổi}.


10. Route Handlers vs Server Actions — which? {Route Handlers vs Server Actions — chọn cái nào?}

Use a Server Action {Dùng Server Action}Use a Route Handler {Dùng Route Handler}
Mutations from your own forms/UI {Mutation từ form/UI của bạn}Webhooks & third-party callbacks {Webhook & callback bên thứ ba}
Progressive-enhancement forms {Form progressive-enhancement}Public REST/JSON APIs {API REST/JSON công khai}
Tight coupling to a component {Gắn chặt với một component}Mobile/native clients {Client mobile/native}
File downloads, streaming, OAuth callbacks {Tải file, streaming, callback OAuth}

Rule of thumb {Nguyên tắc}: own UI → Server Action; outside callers → Route Handler {UI của mình → Server Action; bên ngoài gọi → Route Handler}.


11. Exercises {Bài tập}

  1. JSON API {API JSON}: build GET /api/todos and POST /api/todos backed by an in-memory array, with zod validation on POST {dựng GET /api/todosPOST /api/todos dựa trên mảng in-memory, có validate zod cho POST}.

  2. Dynamic + async params {Dynamic + async params}: add GET /api/todos/[id] that awaits params and returns 404 for unknown ids {thêm GET /api/todos/[id] await params và trả 404 cho id lạ}.

  3. Cookies {Cookie}: build a POST /api/login that sets an httpOnly session cookie and a GET /api/me that reads it with await cookies() {dựng POST /api/login set cookie session httpOnly và GET /api/me đọc nó bằng await cookies()}.

  4. Streaming {Streaming}: build a route that streams 5 chunks with a 500ms gap and watch them arrive incrementally with curl -N {dựng một route stream 5 chunk cách nhau 500ms và xem chúng đến dần với curl -N}.

  5. Proxy gate {Chặn bằng proxy}: add a proxy.ts that redirects unauthenticated users away from /dashboard, matched only on that path {thêm proxy.ts chuyển hướng user chưa xác thực khỏi /dashboard, chỉ khớp trên path đó}.

  6. Cache a GET {Cache một GET}: wrap a catalog query in 'use cache' + cacheTag('catalog'), expose it via a route handler, and invalidate it from a Server Action {bọc một truy vấn catalog trong 'use cache' + cacheTag('catalog'), expose qua route handler, và vô hiệu hóa nó từ một Server Action}.


What’s next {Phần tiếp theo}

You can now build real HTTP endpoints, handle cookies/headers/webhooks/streaming, choose runtimes, and gate requests at the network edge with proxy.ts {Giờ bạn dựng được endpoint HTTP thật, xử lý cookie/header/webhook/streaming, chọn runtime, và chặn request ở rìa mạng bằng proxy.ts}.

Part 8 is about how pages are rendered and found: rendering strategies, metadata & SEO, and the built-in asset optimizations (next/image, next/font, next/script, sitemaps, OG images) {Phần 8 nói về cách trang được render và tìm thấy: chiến lược render, metadata & SEO, và các tối ưu asset tích hợp (next/image, next/font, next/script, sitemap, ảnh OG)}.