- 1. Tổng Quan Cấu Trúc Thư Mục Gốc
- 2. Thư Mục app/ - Trái Tim Của Ứng Dụng
- 3. Route Groups (tenNhom) - Tổ Chức Không Ảnh Hưởng URL
- 4. Dynamic Routes - Route Động
- 5. Private Folders _tenFolder - Loại Trừ Khỏi Routing
- 6. Parallel Routes @slot và Intercepting Routes
- 7. Thư Mục api/ và Route Handlers
- 8. Các Thư Mục Ngoài app/ - Tổ Chức Code Dùng Chung
- 9. So Sánh Nhanh: App Router vs Pages Router
- 10. Một Số Lưu Ý Thực Tế Khi Tổ Chức Dự Án
- Kết Luận
Kể từ khi Next.js 13 giới thiệu App Router và chính thức trở thành mặc định từ Next.js 13.4 trở đi, cách tổ chức thư mục trong một dự án Next.js đã thay đổi đáng kể so với Pages Router truyền thống. Bài viết này sẽ đi sâu vào từng phần của cấu trúc thư mục, giải thích ý nghĩa, quy ước đặt tên, và những best practice khi tổ chức dự án thực tế.
1. Tổng Quan Cấu Trúc Thư Mục Gốc
Một dự án Next.js App Router điển hình có cấu trúc như sau:
my-nextjs-app/
├── app/
│ ├── layout.tsx
│ ├── page.tsx
│ ├── globals.css
│ ├── favicon.ico
│ ├── loading.tsx
│ ├── error.tsx
│ ├── not-found.tsx
│ ├── (marketing)/
│ │ ├── about/
│ │ │ └── page.tsx
│ │ └── pricing/
│ │ └── page.tsx
│ ├── (dashboard)/
│ │ ├── layout.tsx
│ │ └── dashboard/
│ │ ├── page.tsx
│ │ └── settings/
│ │ └── page.tsx
│ ├── blog/
│ │ ├── page.tsx
│ │ └── [slug]/
│ │ └── page.tsx
│ └── api/
│ └── users/
│ └── route.ts
├── components/
├── lib/
├── hooks/
├── public/
├── next.config.js
├── package.json
├── tsconfig.json
└── tailwind.config.ts
Nhìn qua có vẻ phức tạp hơn Pages Router, nhưng thực chất mỗi thành phần đều có vai trò rất rõ ràng. Hãy phân tích từng phần.
2. Thư Mục app/ - Trái Tim Của Ứng Dụng
Thư mục app/ là nơi định nghĩa routing dựa trên cấu trúc folder (file-system based routing). Điểm khác biệt cốt lõi so với Pages Router là: mỗi route được đại diện bởi một folder, và bên trong folder đó chứa các file đặc biệt quy định hành vi của route.
2.1. Các File Đặc Biệt (Special Files)
Next.js quy định một tập hợp tên file cố định, mỗi file đảm nhận một chức năng riêng:
| File | Vai trò |
|---|---|
layout.tsx |
Giao diện dùng chung, bọc quanh các page con, không bị re-render khi chuyển route |
page.tsx |
Giao diện chính của route, bắt buộc phải có để route "public" (truy cập được từ URL) |
loading.tsx |
UI hiển thị khi dữ liệu đang tải (tận dụng React Suspense) |
error.tsx |
UI xử lý lỗi runtime trong route đó (phải là Client Component) |
not-found.tsx |
UI hiển thị khi gọi hàm notFound() hoặc route không tồn tại |
route.ts |
Định nghĩa API endpoint (Route Handler), thay thế cho pages/api |
template.tsx |
Tương tự layout nhưng tạo instance mới mỗi lần điều hướng |
default.tsx |
Fallback UI cho Parallel Routes khi không match slot nào |
Điều quan trọng cần nhớ: chỉ file page.tsx mới khiến route trở nên truy cập được qua URL. Nếu một folder chỉ chứa layout.tsx mà không có page.tsx, route đó sẽ không có URL công khai - đây là lý do vì sao ta có thể tạo các folder tổ chức code mà không ảnh hưởng đến URL thực tế (sẽ nói ở phần Route Groups).
2.2. Nested Layouts - Cơ Chế Lồng Ghép Giao Diện
Một trong những cải tiến lớn nhất của App Router là khả năng lồng layout theo cây thư mục. Ví dụ:
app/
├── layout.tsx → Layout gốc (bọc toàn bộ app)
└── dashboard/
├── layout.tsx → Layout riêng cho /dashboard
└── settings/
├── layout.tsx → Layout riêng cho /dashboard/settings
└── page.tsx
Khi người dùng truy cập /dashboard/settings, Next.js sẽ tự động lồng ba layout này vào nhau theo thứ tự: Root Layout → Dashboard Layout → Settings Layout → Page. Cơ chế này giúp tránh lặp code UI (header, sidebar, navigation) mà không cần import thủ công như trước đây.
Một lưu ý quan trọng: layout không re-render khi chuyển route con, giúp giữ nguyên state (ví dụ: vị trí scroll của sidebar, trạng thái form) - đây là lợi thế lớn so với việc mọi thứ đều re-render trong Pages Router.
3. Route Groups (tenNhom) - Tổ Chức Không Ảnh Hưởng URL
Dấu ngoặc đơn quanh tên folder, ví dụ (marketing) hoặc (dashboard), tạo ra một Route Group. Đây là quy ước cho phép:
- Nhóm các route liên quan về mặt logic mà không thêm segment vào URL.
- Áp dụng layout riêng cho từng nhóm route mà không ảnh hưởng cấu trúc URL.
Ví dụ:
app/
├── (marketing)/
│ ├── layout.tsx → Layout cho trang marketing (header, footer công khai)
│ ├── about/page.tsx → URL: /about
│ └── pricing/page.tsx → URL: /pricing
└── (dashboard)/
├── layout.tsx → Layout cho khu vực quản trị (sidebar, auth check)
└── dashboard/page.tsx → URL: /dashboard
Chú ý: URL cuối cùng không chứa (marketing) hay (dashboard) - đây chỉ là công cụ tổ chức code cho developer, hoàn toàn "vô hình" với người dùng cuối. Đây là giải pháp tuyệt vời khi một ứng dụng có nhiều "khu vực" khác nhau (trang công khai, khu vực đăng nhập, khu vực admin) cần layout và logic tách biệt.
4. Dynamic Routes - Route Động
Để tạo route với tham số động, ta dùng dấu ngoặc vuông:
app/
└── blog/
└── [slug]/
└── page.tsx → Khớp với /blog/hello-world, /blog/nextjs-13, ...
Trong page.tsx, tham số slug được truyền vào qua props:
export default function BlogPost({ params }: { params: { slug: string } }) {
return <h1>Bài viết: {params.slug}</h1>;
}
Ngoài dynamic segment cơ bản, còn có hai biến thể nâng cao:
- Catch-all segments
[...slug]: khớp với nhiều segment, ví dụ/blog/2024/07/hello→slug = ['2024', '07', 'hello']. - Optional catch-all
[[...slug]]: giống catch-all nhưng khớp cả khi không có segment nào (tức/blogcũng hợp lệ).
5. Private Folders _tenFolder - Loại Trừ Khỏi Routing
Dấu gạch dưới ở đầu tên folder, ví dụ _components hoặc _lib, báo cho Next.js biết folder này không phải là một route và sẽ bị loại hoàn toàn khỏi hệ thống routing, kể cả các file con bên trong.
app/
└── dashboard/
├── _components/
│ └── Sidebar.tsx
├── _utils/
│ └── formatDate.ts
└── page.tsx
Đây là cách hữu ích để đặt các file hỗ trợ (component, helper function, type definition) ngay cạnh route sử dụng chúng mà không lo Next.js hiểu nhầm thành một route mới.
6. Parallel Routes @slot và Intercepting Routes
6.1. Parallel Routes
Ký hiệu @ tạo ra một "slot" - cho phép render đồng thời nhiều page độc lập trong cùng một layout, hữu ích cho dashboard phức tạp:
app/
├── layout.tsx
├── @team/
│ └── page.tsx
└── @analytics/
└── page.tsx
Trong layout.tsx, các slot được nhận qua props và render song song:
export default function Layout({ children, team, analytics }: {
children: React.ReactNode;
team: React.ReactNode;
analytics: React.ReactNode;
}) {
return (
<>
{children}
{team}
{analytics}
</>
);
}
6.2. Intercepting Routes
Ký hiệu (.), (..), (..)(..), (...) cho phép "chặn" và hiển thị một route trong ngữ cảnh khác (ví dụ: mở ảnh dạng modal khi click từ feed, nhưng vẫn có URL riêng nếu load trực tiếp). Đây là kỹ thuật nâng cao thường thấy trong các ứng dụng kiểu Instagram-clone.
7. Thư Mục api/ và Route Handlers
Thay vì pages/api/*.ts như trước, App Router dùng file route.ts bên trong app/api/:
app/
└── api/
└── users/
└── route.ts
export async function GET(request: Request) {
const users = await db.user.findMany();
return Response.json(users);
}
export async function POST(request: Request) {
const body = await request.json();
const user = await db.user.create({ data: body });
return Response.json(user, { status: 201 });
}
Mỗi HTTP method (GET, POST, PUT, DELETE, PATCH) được export như một hàm riêng trong cùng file, thay vì kiểm tra req.method như trước.
8. Các Thư Mục Ngoài app/ - Tổ Chức Code Dùng Chung
Ngoài app/, một dự án chuyên nghiệp thường có thêm:
components/: Chứa các React component tái sử dụng trên toàn app (Button, Modal, Card...). Nên tách thànhcomponents/ui/cho component thuần UI vàcomponents/shared/cho component nghiệp vụ.lib/: Chứa logic nghiệp vụ, hàm tiện ích, cấu hình client (database client, API client, validation schema).hooks/: Custom React hooks dùng chung.types/: Định nghĩa TypeScript type/interface dùng chung toàn dự án.public/: Static assets (ảnh, favicon, font) được serve trực tiếp qua URL gốc.
Một câu hỏi thường gặp: nên đặt components/ bên trong hay bên ngoài app/? Cả hai cách đều hợp lệ:
- Đặt bên ngoài
app/(ngang hàng): phù hợp khi component dùng chung ở nhiều route khác nhau, dễ import với alias@/components/.... - Đặt bên trong từng route (dùng private folder
_components): phù hợp khi component chỉ phục vụ riêng route đó, giúp code "sống gần" nơi nó được dùng (co-location).
Nhiều team lớn áp dụng chiến lược kết hợp: component thực sự dùng chung (design system) đặt ở components/, còn component đặc thù cho một tính năng đặt ngay trong route đó dưới dạng _components.
9. So Sánh Nhanh: App Router vs Pages Router
| Tiêu chí | Pages Router | App Router |
|---|---|---|
| Thư mục gốc | pages/ |
app/ |
| Định nghĩa route | Tên file = route | Tên folder = route, cần page.tsx |
| Layout dùng chung | _app.tsx, _document.tsx |
layout.tsx lồng nhau theo cây |
| API routes | pages/api/*.ts |
app/api/**/route.ts |
| Loading state | Tự quản lý thủ công | loading.tsx tự động qua Suspense |
| Error handling | _error.tsx toàn cục |
error.tsx theo từng route |
| Server Components | Không hỗ trợ mặc định | Mặc định là Server Component |
10. Một Số Lưu Ý Thực Tế Khi Tổ Chức Dự Án
- Co-location hợp lý: Đừng tách file quá sớm. Nếu một component chỉ dùng ở một route, hãy để nó gần route đó (
_components) thay vì đẩy lêncomponents/chung ngay từ đầu. - Route Groups cho phân quyền: Dùng route group để tách rõ khu vực public/private, ví dụ
(auth)cho login/register,(protected)cho khu vực cần đăng nhập, mỗi nhóm có layout và middleware kiểm tra quyền riêng. - Tránh lạm dụng Parallel/Intercepting Routes: Đây là công cụ mạnh nhưng phức tạp, chỉ nên dùng khi thực sự cần (modal ảnh, dashboard nhiều panel), không nên dùng cho mọi trường hợp.
- Server Component là mặc định: Trong
app/, mọi component mặc định chạy trên server trừ khi khai báo"use client"ở đầu file. Hãy cân nhắc kỹ ranh giới Client/Server để tối ưu bundle size. - Đặt tên file nhất quán: Luôn dùng đúng tên quy ước (
page.tsx,layout.tsx...) vì Next.js nhận diện dựa trên tên file chính xác, sai chính tả sẽ khiến route không hoạt động mà không có cảnh báo rõ ràng.
Kết Luận
Cấu trúc thư mục của App Router tuy phức tạp hơn Pages Router ở lần đầu tiếp cận, nhưng thực chất là một hệ thống quy ước rất mạch lạc: file đặc biệt quy định hành vi, folder quy định route, và các ký hiệu đặc biệt ((), [], _, @, (.)) mở rộng khả năng tổ chức mà không phá vỡ cấu trúc URL. Hiểu rõ từng quy ước này không chỉ giúp tổ chức code sạch hơn mà còn tận dụng tối đa các tính năng như nested layout, streaming UI, và Server Components mà Next.js App Router mang lại.








