jvinhit//lab

Search posts

Type to search across journal entries.

navigate open esc close

Next.js 16 from Zero to Senior · Part 1 — Foundations & the App Router

Start Next.js 16 from zero: what the framework really is, install it with Turbopack, understand the App Router file conventions, and grasp the single most important idea — Server Components by default. With exercises.

This is Part 1 of a 10-part series that takes you from “I’ve heard of Next.js” to senior-level fluency on the latest Next.js (16) {Đây là Phần 1 của series 10 bài đưa bạn từ “mới nghe tới Next.js” đến trình độ senior trên Next.js mới nhất (phiên bản 16)}. Every part is hands-on and ends with exercises {Mỗi phần đều thực hành và kết thúc bằng bài tập}.

A warning up front {Lưu ý trước}: most tutorials you’ll find online describe Next.js 13–15, where caching was implicit and confusing {phần lớn tutorial trên mạng mô tả Next.js 13–15, nơi caching là ngầm định và gây rối}. Next.js 16 changed the defaults in a big way {Next.js 16 đã thay đổi mặc định một cách lớn}. We learn the new model from the start, so you don’t unlearn bad habits later {Ta học mô hình mới ngay từ đầu, để khỏi phải bỏ thói quen xấu về sau}.


1. What Next.js actually is {Next.js thực ra là gì}

React on its own is a library for building UI — it renders components in the browser and nothing more {React tự thân là một thư viện dựng UI — nó render component trong trình duyệt, chỉ vậy}. To ship a real product you also need routing, data fetching, server rendering, bundling, image optimization, and a deployment story {Để ra sản phẩm thật bạn còn cần routing, data fetching, server rendering, bundling, tối ưu ảnh, và cách deploy}.

Next.js is the full-stack React framework that provides all of that as one coherent system {Next.js là framework React full-stack cung cấp tất cả những thứ đó như một hệ thống thống nhất}:

  • File-system routing — folders become URLs, no router config {Routing theo file — thư mục trở thành URL, không cần cấu hình router}.
  • Server-first rendering — components run on the server by default, sending HTML (and minimal JS) to the client {Render ưu tiên server — component chạy trên server theo mặc định, gửi HTML (và rất ít JS) về client}.
  • A backend in the same project — route handlers and Server Actions let you write server code next to your UI {Backend ngay trong cùng project — route handler và Server Action cho phép viết code server cạnh UI}.
  • Build tooling — Turbopack (Rust-based) bundles and hot-reloads your app {Công cụ build — Turbopack (viết bằng Rust) bundle và hot-reload ứng dụng}.

The mental model for the whole series {Mô hình tư duy cho cả series}: Next.js is a server that renders React, and only ships JavaScript to the browser when a piece of UI truly needs interactivity {Next.js là một server render React, và chỉ gửi JavaScript về trình duyệt khi một mẩu UI thực sự cần tương tác}.

Two routers: why we only use the App Router {Hai router: vì sao ta chỉ dùng App Router}

Next.js historically had two routers {Next.js trong lịch sử có hai router}:

  • The Pages Router (pages/) — the original, client-rendered-by-default model {Pages Router (pages/) — mô hình gốc, render phía client theo mặc định}.
  • The App Router (app/) — the modern model built on React Server Components {App Router (app/) — mô hình hiện đại xây trên React Server Components}.

This series is 100% App Router {Series này dùng 100% App Router}. The Pages Router still works for legacy apps, but every new feature in Next.js 16 — Cache Components, PPR, async APIs — lives in the App Router {Pages Router vẫn chạy cho app cũ, nhưng mọi tính năng mới của Next.js 16 — Cache Components, PPR, async API — đều nằm ở App Router}.


2. Install & create your first app {Cài đặt & tạo app đầu tiên}

You need Node.js 20.9+ (Next.js 16 dropped older versions) {Bạn cần Node.js 20.9+ (Next.js 16 đã bỏ các bản cũ hơn)}. Check it {Kiểm tra}:

node -v   # must be >= 20.9

Scaffold a project with the official CLI {Tạo project bằng CLI chính thức}:

npx create-next-app@latest senior-next

You’ll be asked a few questions. For this series, choose {Bạn sẽ được hỏi vài câu. Cho series này, chọn}:

✔ Would you like to use TypeScript?        … Yes
✔ Would you like to use ESLint?            … Yes
✔ Would you like to use Tailwind CSS?      … Yes
✔ Would you like your code inside a `src/` directory? … Yes
✔ Would you like to use App Router?        … Yes   (the default)
✔ Would you like to use Turbopack?         … Yes   (now the default)

Then run the dev server {Sau đó chạy dev server}:

cd senior-next
npm run dev

Open http://localhost:3000 and you’ll see the starter page {Mở http://localhost:3000, bạn sẽ thấy trang khởi tạo}.

Turbopack is the default now {Turbopack giờ là mặc định}

In Next.js 16, Turbopack is the default bundler for both dev and build — webpack is no longer used unless you opt in {Ở Next.js 16, Turbopack là bundler mặc định cho cả devbuild — webpack không còn được dùng trừ khi bạn chủ động chọn}. Your package.json scripts are simply {Script trong package.json đơn giản là}:

{
  "scripts": {
    "dev": "next dev",
    "build": "next build",
    "start": "next start"
  }
}

If a webpack-only plugin breaks during migration, you can temporarily fall back {Nếu một plugin chỉ hỗ trợ webpack bị lỗi khi migrate, bạn có thể tạm quay về}:

next dev --webpack    # opt out of Turbopack (escape hatch)

But for new apps: just use the defaults — Turbopack gives sub-200ms hot reloads even on large module graphs {Nhưng với app mới: cứ dùng mặc định — Turbopack cho hot reload dưới 200ms kể cả với graph module lớn}.


3. The shape of an App Router project {Hình hài một project App Router}

After scaffolding, the important parts look like this {Sau khi tạo, các phần quan trọng trông như sau}:

senior-next/
├── src/
│   └── app/
│       ├── layout.tsx        # root layout — wraps every page (required)
│       ├── page.tsx          # the "/" route
│       ├── globals.css       # global styles
│       └── favicon.ico
├── public/                   # static files served as-is (/logo.png)
├── next.config.ts            # framework configuration
├── tsconfig.json
└── package.json

The golden rule of the App Router {Quy tắc vàng của App Router}: a folder is a route segment, and special files give that segment behavior {một thư mục là một route segment, và các file đặc biệt gán hành vi cho segment đó}.

The special files (memorize these) {Các file đặc biệt (hãy nhớ)}

FileRole {Vai trò}
page.tsxThe UI for a route — makes the segment publicly routable {UI cho một route — làm segment có thể truy cập}
layout.tsxShared shell that wraps a page and its children; preserves state on navigation {Khung dùng chung bọc page và con; giữ state khi điều hướng}
loading.tsxInstant loading UI (a Suspense fallback) {UI loading tức thì (fallback của Suspense)}
error.tsxError boundary for the segment (must be a Client Component) {Error boundary cho segment (phải là Client Component)}
not-found.tsxUI for 404s within the segment {UI cho 404 trong segment}
route.tsAn API endpoint (no UI) — covered in Part 7 {Một API endpoint (không UI) — xem Phần 7}
template.tsxLike layout but re-mounts on every navigation {Giống layout nhưng mount lại mỗi lần điều hướng}

We go deep on routing in Part 2 {Ta sẽ đào sâu routing ở Phần 2}. For now, just two files matter {Bây giờ chỉ hai file quan trọng}.

The root layout {Root layout}

Every App Router app must have a root layout.tsx that renders <html> and <body> {Mọi app App Router bắt buộclayout.tsx gốc render <html><body>}:

// src/app/layout.tsx
import type { Metadata } from 'next';
import './globals.css';

export const metadata: Metadata = {
  title: 'Senior Next',
  description: 'Learning Next.js 16 properly',
};

export default function RootLayout({
  children,
}: {
  children: React.ReactNode;
}) {
  return (
    <html lang="en">
      <body>{children}</body>
    </html>
  );
}

children is whatever page (or nested layout) is currently active {children là page (hoặc layout lồng) đang active}. The layout renders once and stays mounted as the user navigates between its child pages {Layout render một lần và giữ nguyên khi user điều hướng giữa các page con}.

A page {Một page}

// src/app/page.tsx
export default function HomePage() {
  return (
    <main>
      <h1>Hello from a Server Component</h1>
    </main>
  );
}

That’s it — no router registration, no imports, no export const path {Chỉ vậy — không đăng ký router, không import, không export const path}. The file’s location is the route {Vị trí file chính là route}.


4. The most important idea: Server Components by default {Ý tưởng quan trọng nhất: Server Components là mặc định}

This is the concept that separates people who use the App Router from people who understand it {Đây là khái niệm tách biệt người dùng App Router với người hiểu nó}.

Every component in app/ is a React Server Component (RSC) unless you say otherwise {Mọi component trong app/ là một React Server Component (RSC) trừ khi bạn nói khác đi}.

A Server Component {Một Server Component}:

  • Runs on the server only — its code is never sent to the browser {Chạy chỉ trên server — code của nó không bao giờ gửi về trình duyệt}.
  • Can be async and await data directly (databases, files, APIs) {Có thể asyncawait dữ liệu trực tiếp (database, file, API)}.
  • Can safely use secrets (API keys, DB credentials) — they stay on the server {Có thể dùng secret an toàn (API key, DB credential) — chúng ở lại server}.
  • Cannot use useState, useEffect, onClick, or browser APIs {Không thể dùng useState, useEffect, onClick, hay browser API}.

Here’s a Server Component fetching data with no useEffect, no loading state, no API route {Đây là một Server Component fetch dữ liệu không cần useEffect, không cần loading state, không cần API route}:

// src/app/page.tsx  (a Server Component — the default)
export default async function HomePage() {
  // This runs on the server. The browser never sees this fetch,
  // never sees the URL, never sees any token you might attach.
  const res = await fetch('https://api.example.com/stats');
  const stats = await res.json();

  return (
    <main>
      <h1>Dashboard</h1>
      <p>Users: {stats.users}</p>
    </main>
  );
}

The server renders this to HTML and streams it to the browser {Server render cái này thành HTML rồi stream về trình duyệt}. The user sees content immediately, and zero JavaScript is shipped for this component {User thấy nội dung ngay, và không một byte JavaScript nào được gửi cho component này}.

When you need interactivity: 'use client' {Khi cần tương tác: 'use client'}

The moment you need useState, an event handler, or a browser API, you opt that component into being a Client Component with a directive at the top of the file {Khoảnh khắc bạn cần useState, một event handler, hay browser API, bạn chọn component đó thành Client Component bằng một directive ở đầu file}:

// src/app/counter.tsx
'use client';

import { useState } from 'react';

export default function Counter() {
  const [count, setCount] = useState(0);
  return <button onClick={() => setCount(count + 1)}>Count: {count}</button>;
}

'use client' marks the boundary where server rendering hands off to the browser {'use client' đánh dấu ranh giới nơi server render bàn giao cho trình duyệt}. Everything imported into a Client Component also ships to the browser {Mọi thứ được import vào một Client Component cũng được gửi về trình duyệt}.

We dedicate Part 5 to Client Components {Ta dành hẳn Phần 5 cho Client Components}. The key takeaway now {Điều cốt lõi bây giờ}: default to Server Components; reach for 'use client' only at the leaves of your tree that need interactivity {mặc định dùng Server Components; chỉ dùng 'use client' ở các “lá” của cây cần tương tác}.

   Server Component (page)         ← runs on server, ships 0 JS
     ├── Server Component (header)
     ├── Server Component (article)
     └── Client Component (LikeButton)   ← 'use client' boundary, ships JS

5. Dynamic by default — the Next.js 16 reset {Dynamic theo mặc định — sự thiết lập lại của Next.js 16}

Here’s the big behavioral change from older tutorials {Đây là thay đổi hành vi lớn so với các tutorial cũ}.

In Next.js 13–15, fetch() results were cached automatically, which meant your data could be silently stale and you’d fight the framework to get fresh data {Ở Next.js 13–15, kết quả fetch() được cache tự động, nghĩa là dữ liệu có thể cũ một cách âm thầm và bạn phải “chiến đấu” với framework để lấy dữ liệu mới}.

In Next.js 16, nothing is cached unless you opt in {Ở Next.js 16, không gì được cache trừ khi bạn chủ động chọn}. By default, dynamic code (anything that fetches data or reads the request) runs at request time, like a normal server {Theo mặc định, code động (bất cứ thứ gì fetch dữ liệu hay đọc request) chạy lúc có request, như một server bình thường}.

// Next.js 16: this fetches fresh on every request by default.
const res = await fetch('https://api.example.com/price');

To make something fast and cacheable, you explicitly mark it with the new 'use cache' directive {Để làm gì đó nhanh và cache được, bạn chủ động đánh dấu nó bằng directive mới 'use cache'}:

async function getPrice() {
  'use cache'; // opt IN to caching — covered in depth in Part 4
  const res = await fetch('https://api.example.com/price');
  return res.json();
}

Don’t worry about the details yet — Part 4 is entirely about Cache Components {Đừng lo về chi tiết — Phần 4 dành trọn cho Cache Components}. The mindset to lock in now {Tư duy cần ghi nhớ ngay}: caching is an explicit, opt-in decision in Next.js 16, not a hidden default {caching là một quyết định chủ động, tự chọn trong Next.js 16, không phải mặc định ẩn}.


6. Reading the dev output {Đọc output của dev server}

When you run npm run dev, learn to read the logs {Khi chạy npm run dev, hãy học cách đọc log}:

  ▲ Next.js 16.2.0 (Turbopack)
  - Local:        http://localhost:3000
  - Environments: .env.local

 ✓ Starting...
 ✓ Ready in 1.1s
 ○ Compiling / ...
 ✓ Compiled / in 320ms
 GET / 200 in 28ms

Next.js 16 improved request logging {Next.js 16 cải thiện log request}. A GET / 200 line per request tells you the route, status, and timing {Một dòng GET / 200 cho mỗi request cho bạn biết route, status và thời gian}. When something is slow, this is your first clue {Khi có gì chậm, đây là manh mối đầu tiên}.

For deeper inspection, Next.js 16 ships a Devtools MCP server you can connect to your AI tools for debugging — we’ll use it in Part 10 {Để soi sâu hơn, Next.js 16 có Devtools MCP server bạn có thể kết nối với công cụ AI để debug — ta sẽ dùng ở Phần 10}.


7. next.config.ts — the control panel {next.config.ts — bảng điều khiển}

Configuration lives in a typed file {Cấu hình nằm trong một file có type}:

// next.config.ts
import type { NextConfig } from 'next';

const nextConfig: NextConfig = {
  // We'll enable Cache Components in Part 4:
  // cacheComponents: true,
  images: {
    remotePatterns: [{ protocol: 'https', hostname: 'images.example.com' }],
  },
};

export default nextConfig;

You don’t need to touch most options {Bạn không cần đụng phần lớn tùy chọn}. The one you’ll meet soon is cacheComponents: true, which unlocks the 'use cache' directive (it’s a no-op without this flag) {Cái bạn sẽ gặp sớm là cacheComponents: true, nó mở khóa directive 'use cache' (không bật cờ này thì directive vô tác dụng)}.


8. Exercises {Bài tập}

Do these before Part 2 — building muscle memory matters more than reading {Làm trước khi sang Phần 2 — tạo phản xạ quan trọng hơn đọc}.

  1. Scaffold & run {Tạo & chạy}: create the app with create-next-app, run npm run dev, and confirm Turbopack is in the startup banner {tạo app bằng create-next-app, chạy npm run dev, xác nhận có Turbopack trong banner khởi động}.

  2. Two routes {Hai route}: add src/app/about/page.tsx so that /about renders an <h1>About</h1>. No config needed — prove the file-system routing to yourself {thêm src/app/about/page.tsx để /about render <h1>About</h1>. Không cần cấu hình — tự chứng minh routing theo file}.

  3. Server fetch {Fetch trên server}: in page.tsx, await fetch('https://api.github.com/repos/vercel/next.js') and render the stargazers_count. Open DevTools → Network and confirm the browser never made that request {trong page.tsx, await fetch(...) và render stargazers_count. Mở DevTools → Network và xác nhận trình duyệt không hề gửi request đó}.

  4. Find the boundary {Tìm ranh giới}: try adding useState to your page.tsx (a Server Component). Read the error {thử thêm useState vào page.tsx. Đọc lỗi}. Then fix it by extracting an interactive Counter into its own 'use client' file and importing it {Rồi sửa bằng cách tách Counter tương tác ra file 'use client' riêng và import vào}.

  5. Predict the JS {Dự đoán JS}: before checking, predict which components from exercise 4 ship JavaScript to the browser, then verify in the Network tab {trước khi kiểm tra, dự đoán component nào ở bài 4 gửi JavaScript về trình duyệt, rồi xác nhận trong tab Network}.


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

You can now scaffold an app, you understand the App Router file conventions, and you’ve internalized the single most important idea: Server Components by default, Client Components at the interactive leaves, and caching is opt-in {Giờ bạn đã tạo được app, hiểu các quy ước file của App Router, và nắm ý tưởng quan trọng nhất: Server Components là mặc định, Client Components ở các lá tương tác, và caching là tự chọn}.

In Part 2, we go deep on routing — dynamic segments, route groups, parallel and intercepting routes, layouts vs templates, and the loading/error/not-found files that make your app feel instant {Ở Phần 2, ta đào sâu routing — dynamic segment, route group, parallel & intercepting route, layout vs template, và các file loading/error/not-found giúp app cảm giác tức thì}.