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ả dev và build — 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ớ)}
| File | Role {Vai trò} |
|---|---|
page.tsx | The UI for a route — makes the segment publicly routable {UI cho một route — làm segment có thể truy cập} |
layout.tsx | Shared 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.tsx | Instant loading UI (a Suspense fallback) {UI loading tức thì (fallback của Suspense)} |
error.tsx | Error boundary for the segment (must be a Client Component) {Error boundary cho segment (phải là Client Component)} |
not-found.tsx | UI for 404s within the segment {UI cho 404 trong segment} |
route.ts | An API endpoint (no UI) — covered in Part 7 {Một API endpoint (không UI) — xem Phần 7} |
template.tsx | Like 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ộc có layout.tsx gốc render <html> và <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
asyncandawaitdata directly (databases, files, APIs) {Có thểasyncvàawaitdữ 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ùnguseState,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}.
-
Scaffold & run {Tạo & chạy}: create the app with
create-next-app, runnpm run dev, and confirm Turbopack is in the startup banner {tạo app bằngcreate-next-app, chạynpm run dev, xác nhận có Turbopack trong banner khởi động}. -
Two routes {Hai route}: add
src/app/about/page.tsxso that/aboutrenders an<h1>About</h1>. No config needed — prove the file-system routing to yourself {thêmsrc/app/about/page.tsxđể/aboutrender<h1>About</h1>. Không cần cấu hình — tự chứng minh routing theo file}. -
Server fetch {Fetch trên server}: in
page.tsx,await fetch('https://api.github.com/repos/vercel/next.js')and render thestargazers_count. Open DevTools → Network and confirm the browser never made that request {trongpage.tsx,await fetch(...)và renderstargazers_count. Mở DevTools → Network và xác nhận trình duyệt không hề gửi request đó}. -
Find the boundary {Tìm ranh giới}: try adding
useStateto yourpage.tsx(a Server Component). Read the error {thử thêmuseStatevàopage.tsx. Đọc lỗi}. Then fix it by extracting an interactiveCounterinto its own'use client'file and importing it {Rồi sửa bằng cách táchCountertương tác ra file'use client'riêng và import vào}. -
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ì}.