export default async function Page(props: PageProps<'/blog/[slug]'>) {
const { slug } = await props.params;
const { page = '1' } = await props.searchParams;
return <Post slug={slug} page={Number(page)} />;
}
Next.js
draftedApp Router vs Pages Router, Server/Client boundary, rendering (prerender, streaming, PPR), caching & revalidation theo từng version (14 → 15 → 16 Cache Components), Server Actions security, proxy (middleware cũ), auth, hydration, self-hosting — theo Next.js 16.
0/65 rated · mastery 0%
Bài học
11 bài · ~141 phút đọc. Học theo thứ tự, rồi làm câu hỏi.- 01~16 phút· 5 câuverify
App Router: routing, file conventions & navigation
App Router khác Pages Router ở đâu, cây segment và thứ tự lồng của file conventions (layout, template, error, loading, not-found, page), client-side navigation và Link prefetch, parallel + intercepting routes cho modal có URL, và cách migrate từng route.
- 02~14 phút· 7 câuverify
Server Components & Client Components
Server/Client boundary thực chất là ranh giới module graph: 'use client' làm gì, RSC payload chứa gì (đã xem payload thật), props nào qua được ranh giới, pattern children slot, context provider, hydration mismatch theo timezone và cách chia component khi migrate.
- 03~13 phút· 7 câuverify
Rendering: prerender, dynamic, streaming & async request APIs
Prerender vs dynamic rendering và điều gì làm route thành dynamic, async params/searchParams/cookies ở Next 16, generateStaticParams và dynamicParams, loading.tsx vs Suspense (đã đo timing chunk thật), waterfall 1.8s → 0.6s, generateMetadata, HTTP status khi stream và bot.
- 04~12 phút· 4 câuverify
Caching theo version: Next 14 → 15 → 16 và mô hình cũ
Các tầng cache của Next (memoization, data cache, prerender/full route, client router cache), defaults đổi thế nào qua 14/15/16, scope của fetch memoization vs React.cache vs unstable_cache vs 'use cache', Route Handler caching, và khi nào vẫn fetch ở client — kèm số đo thật.
- 05~15 phút· 7 câuverify
Cache Components: 'use cache', cacheLife, cacheTag & PPR
Mô hình Next 16 khi bật cacheComponents: static shell + dynamic holes (PPR), 'use cache' và cache key, cacheLife/cacheTag, runtime data không được vào cache, Math.random/Date.now, 'use cache: private/remote', Activity giữ state, đẩy dynamic access xuống sâu và cách rollout — với lỗi build/runtime thật từ 16.3.7.
- 06~13 phút· 5 câuverify
Revalidation & chiến lược cache theo trang
revalidateTag vs updateTag vs revalidatePath vs refresh ở Next 16 (đã đo SWR thật), tag hệ thống và soft tag, invalidation từ webhook, xếp chồng nhiều tầng cache (browser → CDN → Next → Redis → DB), và chọn rendering/caching cho từng loại trang e-commerce, search và storefront multi-tenant.
- 07~11 phút· 6 câuverify
Server Actions, forms & error handling
Server Action là một POST endpoint (đã gọi thẳng bằng curl với action id), useActionState và progressive enhancement, lỗi mong đợi vs bất ngờ, error.tsx/global-error/catchError (retry ở 16.3), redirect trong try/catch (NEXT_REDIRECT thật), sequential dispatch, và chọn Server Action hay Route Handler cho checkout.
- 08~11 phút· 4 câuverify
Proxy (middleware cũ): matcher, rewrite & giới hạn
proxy.ts ở Next 16 (đổi tên từ middleware, Node runtime), thứ tự thực thi, matcher và các lỗi hay gặp (đã chạy thật: CSS bị redirect, prefetch bypass lộ payload), rewrite multi-tenant theo host, CVE-2025-29927 và bài học defense in depth.
- 09~12 phút· 7 câuverify
Authentication, DAL & data security
Kiến trúc auth khuyến nghị cho App Router: session cookie, Data Access Layer với verifySession, DTO, server-only và taint (đã chạy thật), vì sao check trong layout/proxy không đủ (slot @admin lộ trong payload thật), bảo vệ Server Action, DAL trực tiếp vs API riêng (BFF), OAuth/JWT refresh, và checklist security review.
- 10~11 phút· 6 câuverify
Env vars, images, fonts & toolchain Next 16
NEXT_PUBLIC_ bị inline lúc build và cờ server bị bake vào trang prerender (đã chạy thật với một Docker-style build), runtime config cho một image nhiều môi trường, next/image và next/font cùng các default mới ở v16, Turbopack mặc định và lỗi webpack config, next lint bị xoá, static export mất gì, và dùng AI assistant với docs đúng version.
- 11~13 phút· 7 câuverify
Self-hosting & vận hành production
Chạy Next 16 ngoài Vercel: output standalone, cache per-instance và cache handler dùng chung (đã chạy 2 pod thật), version skew với deploymentId và encryption key, streaming sau reverse proxy (đo proxy buffer thật), after() và graceful shutdown, observability với instrumentation/OpenTelemetry, và cách debug một trang chậm theo từng tầng.
Tóm tắt & cheat sheet của track
TL;DR
- Next.js 16 (bản đang cài: 16.3) thay đổi nhiều so với kiến thức cũ:
middlewaređổi tên thànhproxy(chạy Node.js runtime),cookies()/headers()/params/searchParamschỉ còn async, Turbopack mặc định, và mô hình cache mới Cache Components (cacheComponents: true+'use cache',cacheLife,cacheTag,updateTag) — PPR trở thành mặc định khi bật nó. - App Router mặc định là Server Components: code chạy trên server, không ship JS; chỉ phần có
'use client'được hydrate. Ranh giới server/client là nơi sinh ra phần lớn bug (props không serializable, lộ dữ liệu, import nhầm module server). - Caching là chủ đề hay bị hỏi nhất và phụ thuộc version: Next 14 cache
fetchmặc định; Next 15 đảo lại (không cache mặc định); Next 16 thêm Cache Components opt-in. Luôn hỏi lại "version nào, có bật cacheComponents không". - Server Actions là public POST endpoint: phải tự xác thực + phân quyền trong từng action, validate input, chỉ trả dữ liệu cần. Proxy chỉ là optimistic check.
- Self-hosting nhiều instance cần: shared cache handler (+ đồng bộ tag),
NEXT_SERVER_ACTIONS_ENCRYPTION_KEYchung,deploymentIdchống version skew, proxy không buffer để streaming hoạt động.
Khái niệm cốt lõi
| Khái niệm | Một câu | Ví dụ ngắn |
|---|---|---|
| Server Component | Render trên server thành RSC payload, không có state/effect, đọc DB trực tiếp được | async function Page() { const o = await db.order.findMany() } |
| Client Component | File có 'use client' và mọi thứ nó import; được SSR rồi hydrate | nút Like dùng useState |
| RSC payload | Dạng serialize của cây Server Components + chỗ đặt Client Components + props | dùng cho navigation phía client |
| Prerender | Render lúc build/revalidate, kết quả được cache (static shell) | trang marketing |
| Dynamic rendering | Render lúc request vì dùng runtime data | trang đọc cookies() |
| Streaming | Gửi HTML theo từng <Suspense> boundary | loading.tsx → skeleton trước |
| PPR | Một route = static shell (prerender) + dynamic holes (stream) | header tĩnh + giỏ hàng động |
'use cache' | Cache kết quả function/component; args thành cache key | getProducts() cache hours |
| Server Action | Server Function gọi qua <form action>/transition; là POST tới route | createPost(formData) |
| Proxy | Code chạy trước request tới route (rewrite, redirect, header) | redirect /login nếu thiếu cookie |
Cơ chế hoạt động
Server/Client boundary và first load
Cache Components (Next 16, cacheComponents: true) quyết định static shell
Mutation với Server Action (một round-trip)
Caching theo version (tóm tắt, cần verify với docs của version bạn dùng)
| Next 14 | Next 15 | Next 16 (không bật cacheComponents) | Next 16 + cacheComponents | |
|---|---|---|---|---|
fetch mặc định | cached (force-cache) | không cache | không cache | không cache; dùng 'use cache' |
| GET Route Handler | cached mặc định | không cache mặc định | không cache | theo mô hình prerender như page |
| Client router cache (page động) | 30s | 0s (staleTimes.dynamic) | 0s | theo cacheLife.stale (tối thiểu 30s) |
| Request APIs | sync | async (còn sync compat) | chỉ async | chỉ async |
| Công cụ cache non-fetch | unstable_cache | unstable_cache | unstable_cache | 'use cache' + cacheLife/cacheTag |
Segment config dynamic/revalidate | có | có | có | lỗi — thay bằng use cache/Suspense |
Trade-offs
| Quyết định | Lựa chọn | Đánh đổi |
|---|---|---|
| Server Component vs Client Component | Server mặc định; client chỉ ở lá cần tương tác | Client lớn → bundle lớn; server → không có state/effect, cần round-trip để cập nhật |
| Server Action vs Route Handler | Action cho mutation từ UI của chính app; Route Handler cho webhook, API public, client ngoài, GET cacheable | Action tuần tự trên client, gắn với build (action id); Route Handler phải tự lo CSRF/format |
use cache vs Suspense stream | Cache khi dữ liệu dùng chung, chấp nhận cũ; stream khi phải tươi/theo user | Cache → nguy cơ stale & invalidation; stream → tốn server mỗi request |
revalidateTag vs updateTag | SWR nền vs hết hạn ngay (read-your-writes) | updateTag chỉ trong Server Action, request đó chậm hơn |
| Vercel vs self-host | Managed (CDN, ISR, skew protection sẵn) vs tự quản | Self-host phải tự làm cache handler, streaming, skew, image |
| Proxy vs check trong DAL | Proxy: optimistic, rẻ, chạy mọi route | Không đủ để bảo mật; DAL gần dữ liệu mới là lớp thật |
Pitfalls
- Kiểm tra auth ở
page/layoutrồi nghĩ Server Action trong trang đó được bảo vệ — không, action là endpoint riêng. - Layout không re-render khi navigate giữa các trang con → auth check trong layout không chạy lại.
- Truyền cả record DB (có
passwordHash,internalNote) xuống Client Component → nằm trong RSC payload, xem được bằng DevTools. - Gọi
redirect()/notFound()bên trongtry→ bịcatchnuốt (chúng hoạt động bằng cách throw). - Đọc
cookies()bên trong'use cache'→ lỗi; phải đọc bên ngoài và truyền giá trị làm argument. NEXT_PUBLIC_*bị inline lúc build → một Docker image cho nhiều môi trường sẽ mang giá trị lúc build.- Nhiều pod + cache mặc định (per instance) →
revalidateTagchỉ xoá trên một pod. - Proxy matcher quên loại
_next/static,_next/image→ auth redirect chặn cả CSS/JS.
Cheat sheet
- File conventions:
layout·page·loading(Suspense) ·error(client, boundary) ·global-error(có<html><body>) ·not-found·template(remount mỗi navigation) ·route·default(bắt buộc cho parallel slot ở v16). 'use client'= ranh giới module graph;'use server'= đánh dấu Server Function (không phải Server Component).- Props server → client phải serializable; function chỉ được truyền nếu là Server Action.
cacheLifeprofiles:seconds,minutes,hours,days,weeks,max,default(stale 5m / revalidate 15m / expire never).revalidateTag(tag, 'max')(v16 bắt buộc arg 2) ·updateTag(tag)(chỉ Server Action) ·revalidatePath(path)·refresh()(refetch router, không xoá cache).connection()= "chờ tới request" trước khi đọcMath.random()/env runtime.- Proxy: Node.js runtime, một file
proxy.ts, dùngmatcher; không fetch cache; không dùng thay authorization. - Server Action:
bodySizeLimitmặc định 1MB,allowedOriginskhi sau reverse proxy, closure được mã hoá, ID đổi theo build. - Self-host:
output: 'standalone',cacheHandler/cacheHandlers,deploymentId,NEXT_SERVER_ACTIONS_ENCRYPTION_KEY, nginxX-Accel-Buffering: no, SIGTERM drain.
References
- Next.js — Server and Client Components ↗
- Next.js — Caching (Cache Components) ↗
- Next.js — Caching and Revalidating (Previous Model) ↗
- Next.js — Revalidating ↗
- Next.js — use cache directive ↗
- Next.js — Server Actions guide ↗
- Next.js — Data Security ↗
- Next.js — Authentication ↗
- Next.js — Proxy (file convention) ↗
- Next.js — Self-Hosting ↗
- Next.js — How revalidation works ↗
- Next.js — Upgrading to version 16 ↗
- Next.js — Migrating to Cache Components ↗
- Next.js — Error Handling ↗
- GitHub Advisory — CVE-2025-29927 (middleware authorization bypass) ↗
Interview Questions
Showing 65 / 65
import { cacheLife, cacheTag } from 'next/cache';
export async function getProduct(tenantId: string, slug: string) {
'use cache';
cacheLife('hours');
cacheTag(`product:${tenantId}:${slug}`);
return db.product.findUnique({ where: { tenantId_slug: { tenantId, slug } } });
}
'use server';
import { z } from 'zod';
import { updateTag } from 'next/cache';
import { verifySession } from '@/lib/dal';
const Schema = z.object({ displayName: z.string().min(2).max(50) });
export type State = { errors?: Record<string, string[]>; ok?: boolean };
export async function updateProfile(_prev: State, formData: FormData): Promise<State> {
const session = await verifySession(); // throws/redirects if not logged in
const parsed = Schema.safeParse({ displayName: formData.get('displayName') });
if (!parsed.success) return { errors: parsed.error.flatten().fieldErrors };
await db.user.update({ where: { id: session.userId }, data: parsed.data });
updateTag(`user:${session.userId}`);
return { ok: true };
}
// app/page.tsx (Server Component)
import Modal from './modal'; // 'use client'
import Cart from './cart'; // Server Component, reads DB
export default function Page() {
return <Modal><Cart /></Modal>;
}
export default async function Page(props: PageProps<'/users/[id]'>) {
const { id } = await props.params;
const user = await getUser(id); // 600 ms
const orders = await getOrders(id); // 600 ms
const recommendations = await getRecs(id); // 600 ms
return <Dashboard user={user} orders={orders} recs={recommendations} />;
}
// app/admin/page.tsx
import { auth } from '@/lib/auth';
import { redirect } from 'next/navigation';
import { deleteAllRecords } from './actions';
export default async function AdminPage() {
const session = await auth();
if (!session?.user?.isAdmin) redirect('/login');
return (
<form action={deleteAllRecords}>
<button>Delete all</button>
</form>
);
}
// app/admin/actions.ts
'use server';
export async function deleteAllRecords() {
await db.record.deleteMany();
}
// app/profile/[slug]/page.tsx
export default async function Page(props: PageProps<'/profile/[slug]'>) {
const { slug } = await props.params;
const user = await db.user.findUnique({ where: { slug } }); // full row
return <ProfileCard user={user} />; // 'use client' component
}
import { cookies } from 'next/headers';
async function getTenant() {
const tenantId = (await cookies()).get('tenant')?.value;
return db.tenant.findUnique({ where: { id: tenantId } });
}
export async function TenantBanner() {
'use cache';
const tenant = await getTenant();
return <Banner name={tenant?.name} />;
}
'use server';
import { redirect } from 'next/navigation';
export async function createOrder(_: State, formData: FormData): Promise<State> {
try {
const order = await orders.create(parse(formData));
revalidatePath('/orders');
redirect(`/orders/${order.id}`);
} catch (err) {
return { error: String(err) };
}
}
export default function Layout({ children, params }: LayoutProps<'/shop/[slug]'>) {
return (
<div>
<Sidebar />{/* static / cached */}
<Suspense fallback={<h1>Loading…</h1>}>
{params.then(({ slug }) => <ShopHeading slug={slug} />)}
</Suspense>
<Suspense fallback={<AvatarSkeleton />}><UserMenu /></Suspense>
{children}
</div>
);
}
// lib/config.ts
export const API_URL = process.env.NEXT_PUBLIC_API_URL!;
export const NEW_CHECKOUT = process.env.FEATURE_NEW_CHECKOUT === 'true';
// app/checkout/page.tsx (prerendered at build)
import { NEW_CHECKOUT } from '@/lib/config';
export default function Page() {
return NEW_CHECKOUT ? <NewCheckout /> : <OldCheckout />;
}