Skip to content
DevRecall
← Roadmap
11

Next.js

drafted

App 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.
  1. 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.

  2. 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.

  3. 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.

  4. 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.

  5. 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.

  6. 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.

  7. 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.

  8. 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.

  9. 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. 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. 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ành proxy (chạy Node.js runtime), cookies()/headers()/params/searchParams chỉ 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 fetch mặ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_KEY chung, deploymentId chống version skew, proxy không buffer để streaming hoạt động.

Khái niệm cốt lõi

Khái niệmMột câuVí dụ ngắn
Server ComponentRender trên server thành RSC payload, không có state/effect, đọc DB trực tiếp đượcasync function Page() { const o = await db.order.findMany() }
Client ComponentFile có 'use client' và mọi thứ nó import; được SSR rồi hydratenút Like dùng useState
RSC payloadDạng serialize của cây Server Components + chỗ đặt Client Components + propsdùng cho navigation phía client
PrerenderRender lúc build/revalidate, kết quả được cache (static shell)trang marketing
Dynamic renderingRender lúc request vì dùng runtime datatrang đọc cookies()
StreamingGửi HTML theo từng <Suspense> boundaryloading.tsx → skeleton trước
PPRMộ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 keygetProducts() cache hours
Server ActionServer Function gọi qua <form action>/transition; là POST tới routecreatePost(formData)
ProxyCode 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 14Next 15Next 16 (không bật cacheComponents)Next 16 + cacheComponents
fetch mặc địnhcached (force-cache)không cachekhông cachekhông cache; dùng 'use cache'
GET Route Handlercached mặc địnhkhông cache mặc địnhkhông cachetheo mô hình prerender như page
Client router cache (page động)30s0s (staleTimes.dynamic)0stheo cacheLife.stale (tối thiểu 30s)
Request APIssyncasync (còn sync compat)chỉ asyncchỉ async
Công cụ cache non-fetchunstable_cacheunstable_cacheunstable_cache'use cache' + cacheLife/cacheTag
Segment config dynamic/revalidatecócócólỗi — thay bằng use cache/Suspense

Trade-offs

Quyết địnhLựa chọnĐánh đổi
Server Component vs Client ComponentServer mặc định; client chỉ ở lá cần tương tácClient lớn → bundle lớn; server → không có state/effect, cần round-trip để cập nhật
Server Action vs Route HandlerAction cho mutation từ UI của chính app; Route Handler cho webhook, API public, client ngoài, GET cacheableAction 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 streamCache khi dữ liệu dùng chung, chấp nhận cũ; stream khi phải tươi/theo userCache → nguy cơ stale & invalidation; stream → tốn server mỗi request
revalidateTag vs updateTagSWR nền vs hết hạn ngay (read-your-writes)updateTag chỉ trong Server Action, request đó chậm hơn
Vercel vs self-hostManaged (CDN, ISR, skew protection sẵn) vs tự quảnSelf-host phải tự làm cache handler, streaming, skew, image
Proxy vs check trong DALProxy: optimistic, rẻ, chạy mọi routeKhông đủ để bảo mật; DAL gần dữ liệu mới là lớp thật

Pitfalls

  • Kiểm tra auth ở page/layout rồ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 trong try → bị catch nuố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) → revalidateTag chỉ 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.
  • cacheLife profiles: 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 đọc Math.random()/env runtime.
  • Proxy: Node.js runtime, một file proxy.ts, dùng matcher; không fetch cache; không dùng thay authorization.
  • Server Action: bodySizeLimit mặc định 1MB, allowedOrigins khi sau reverse proxy, closure được mã hoá, ID đổi theo build.
  • Self-host: output: 'standalone', cacheHandler/cacheHandlers, deploymentId, NEXT_SERVER_ACTIONS_ENCRYPTION_KEY, nginx X-Accel-Buffering: no, SIGTERM drain.

Personal notes (Notion)

Ghi chú cá nhân — có thể sai hoặc lỗi thời, dùng để tham khảo.

Interview Questions

Showing 65 / 65

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)} />;
}
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 />;
}