Hướng dẫn chi tiết cách tổ chức Pages, Components và Services trong dự án Next.js — dành cho người non-code
Bài viết trước giới thiệu kiến trúc tổng thể gồm 2 tầng: Frontend (Next.js) và Backend (Firebase). Bài này đi sâu vào tầng Frontend — giải thích cách bố cục và tổ chức một dự án Next.js theo 3 nhóm chính: Pages, Components và Services.
Dành cho ai? Bạn không cần biết code. Bài viết giải thích bằng ngôn ngữ đời thường, dùng ví dụ thực tế từ ứng dụng quản lý công việc (task management). Mục tiêu: giúp bạn hiểu cấu trúc dự án để mô tả yêu cầu cho Claude Code chính xác hơn.
Mỗi nhóm thư mục giải quyết một bài toán khác nhau. Khi cần thay đổi giao diện nút bấm → vào components/. Khi cần thêm trang mới → vào app/. Khi cần sửa cách đọc dữ liệu → vào services/.
Pages là các trang mà người dùng truy cập qua URL. Trong Next.js, mỗi file page.tsx trong thư mục app/ tạo ra một URL tương ứng.
Phép so sánh: Pages giống các phòng trong nhà. Phòng khách (trang chủ), phòng làm việc (dashboard), phòng hồ sơ (danh sách tasks). Mỗi phòng có địa chỉ riêng (URL) và mục đích riêng.
Không cần cấu hình routing — thêm thư mục mới = có URL mới.
[id] — Trang động (Dynamic Route)
Dấu ngoặc vuông [id] tạo trang dùng cho nhiều URL khác nhau. Thay vì tạo riêng 1 trang cho mỗi task, bạn tạo 1 trang duy nhất hiển thị nội dung theo ID.
app/dashboard/tasks/[id]/page.tsx→ yourapp.com/dashboard/tasks/task_001 ← Hiển thị task "Làm báo cáo Q1"→ yourapp.com/dashboard/tasks/task_002 ← Hiển thị task "Review thiết kế"→ yourapp.com/dashboard/tasks/task_003 ← Hiển thị task "Họp với khách hàng"
Cùng 1 file page.tsx nhưng hiển thị nội dung khác nhau dựa trên id trong URL.
(auth) — Nhóm route (Route Group)
Dấu ngoặc tròn (auth) tạo nhóm logic nhưng không ảnh hưởng URL.
app/(auth)/login/page.tsx → yourapp.com/login (không có /auth/)app/(auth)/register/page.tsx → yourapp.com/register (không có /auth/)
Dùng để nhóm các trang liên quan (ví dụ tất cả trang xác thực) mà không thêm thư mục phụ vào URL. Giúp project gọn gàng hơn.
layout.tsx — Khung chung cho nhiều trang
Layout là phần giao diện bao bọc các trang con, giữ nguyên khi chuyển trang.
app/layout.tsx ← Layout gốc: Header, font, theme (áp dụng tất cả trang)app/dashboard/layout.tsx ← Layout dashboard: Sidebar (áp dụng mọi trang /dashboard/*)
Ví dụ: Khi người dùng chuyển từ /dashboard/tasks sang /dashboard/projects, Sidebar giữ nguyên — chỉ phần nội dung chính thay đổi.
Thư mục api/ cho phép tạo API endpoint ngay trong dự án Next.js. Code này chạy trên server (Vercel), không lộ ra trình duyệt — phù hợp cho logic cần giữ bí mật.
┌─────────────────────────────────┐│ 1. Lấy dữ liệu │ ← Gọi Service để đọc từ Firebase│ (getProjects, getTasks) │├─────────────────────────────────┤│ 2. Ghép các Components │ ← Lắp ráp từ khối có sẵn│ TaskFilter ││ TaskTable data={tasks} ││ CreateTaskButton │├─────────────────────────────────┤│ 3. Trả về giao diện hoàn chỉnh│ ← Next.js hiển thị trang└─────────────────────────────────┘
Page không tự vẽ giao diện chi tiết. Nó lấy dữ liệu từ Services, rồi ghép các Components lại thành trang hoàn chỉnh — giống người quản lý sắp xếp nội thất vào phòng.
Components là mảnh ghép giao diện có thể tái sử dụng. Tạo một lần, ghép vào nhiều trang khác nhau, và khi sửa thì chỉ cần sửa ở một chỗ.
Phép so sánh: Components giống nội thất trong nhà. Một chiếc bàn (component Table) có thể đặt ở phòng khách, phòng ăn, hoặc phòng làm việc. Nếu muốn đổi kiểu bàn, bạn thay 1 lần — các phòng tự cập nhật.
Đây là các component nhỏ nhất, đơn giản nhất — nút bấm, ô input, badge, dropdown. Thường lấy từ thư viện shadcn/ui (bộ component có sẵn, được tùy biến theo dự án).
Component
Công dụng
Button
Nút bấm (Lưu, Xóa, Thêm mới)
Input
Ô nhập liệu (tiêu đề, mô tả)
Select
Dropdown (chọn trạng thái, chọn người)
Badge
Nhãn nhỏ (In Progress, Completed)
Dialog
Popup/modal (form tạo task, xác nhận xóa)
Table
Bảng dữ liệu (danh sách tasks)
Avatar
Ảnh đại diện tròn
DatePicker
Chọn ngày (deadline)
Đặc điểm: Không biết gì về nghiệp vụ — Button chỉ biết nó là nút bấm, không biết bấm để làm gì. Tính năng do component cha quyết định.
Feature Components — Khối nghiệp vụ
Feature components ghép nhiều UI components lại để tạo thành một khối chức năng cụ thể. Chúng hiểu nghiệp vụ, biết dữ liệu trông như thế nào.Ví dụ — TaskCard ghép từ nhiều UI components:
┌──────────────────────────────────────┐│ ☑️ Làm báo cáo Q1 │ ← Checkbox + Text│ ││ 👤 Nguyễn Văn A 🏷️ In Progress │ ← Avatar + Badge│ 📅 30/04/2026 │ ← DatePicker (read-only)│ [Sửa] [Xóa] │ ← Buttons└──────────────────────────────────────┘
TaskCard sử dụng Avatar, Badge, Button, Checkbox — nhưng biết cách hiển thị thông tin task cụ thể.Ví dụ — TaskForm ghép từ nhiều UI components:
Services là code giao tiếp với Firebase (hoặc bất kỳ nguồn dữ liệu nào). Chúng chứa các hàm đọc/ghi dữ liệu mà Pages và Components gọi đến khi cần.
Phép so sánh: Services giống hệ thống kỹ thuật trong nhà — đường ống nước, dây điện, đường dây internet. Bạn không nhìn thấy chúng, nhưng khi vặn vòi nước (bấm nút lấy dữ liệu) thì nước chảy (dữ liệu hiện ra). Nếu cần sửa ống nước, bạn sửa ở hệ thống kỹ thuật — không cần đập tường phòng khách.
Một số dự án đặt Services trong thư mục lib/ thay vì services/. Cả hai cách đều đúng — điều quan trọng là tách riêng code kết nối dữ liệu ra khỏi giao diện.
Hooks là đoạn logic mà nhiều components cùng cần, được gói gọn vào hàm riêng để tái sử dụng.
hooks/├── useAuth.ts ← Quản lý trạng thái đăng nhập│ (user hiện tại, đang loading, đã login chưa)│├── useTasks.ts ← Quản lý dữ liệu tasks│ (danh sách tasks, loading, lỗi, hàm tạo/sửa/xóa)│└── useDebounce.ts ← Trì hoãn thao tác (tìm kiếm khi ngừng gõ)
Ví dụ thực tế: Cả trang Dashboard và trang Tasks đều cần biết “user hiện tại là ai”. Thay vì viết lại logic đọc thông tin user ở 2 nơi, dùng useAuth() — gọi 1 dòng, có ngay thông tin user.
Types mô tả dữ liệu trông như thế nào — giúp Claude Code viết code chính xác hơn và phát hiện lỗi sớm.
types/├── task.ts ← Task gồm những trường nào?│ title: string (tiêu đề)│ status: "todo" | "in_progress" | "completed"│ assignee: string (ID người phụ trách)│ deadline: Date (ngày hết hạn)│└── user.ts ← User gồm những trường nào? name: string email: string role: "admin" | "member" avatarUrl: string
Bạn không cần viết Types — Claude Code sẽ tự tạo dựa trên mô tả dữ liệu của bạn. Nhưng biết thư mục này tồn tại giúp bạn hiểu dự án rõ hơn.
Khi bạn hiểu cấu trúc Pages / Components / Services, việc mô tả yêu cầu cho Claude Code trở nên chính xác hơn:Thêm trang mới:
"Tạo trang /dashboard/reports tại app/dashboard/reports/page.tsx.Trang này hiển thị thống kê: tổng số tasks, tỷ lệ hoàn thành, tasks quá hạn.Lấy dữ liệu từ service tasks.ts."
Thêm component:
"Tạo component TaskKanban trong components/tasks/TaskKanban.tsx.Hiển thị tasks dạng kanban board với 3 cột: Todo, In Progress, Completed.Mỗi task hiển thị bằng TaskCard (component có sẵn).Cho phép kéo-thả task giữa các cột.Khi kéo-thả → gọi updateTask() từ services/tasks.ts để cập nhật status."
Thêm service:
"Tạo service mới services/notifications.ts.Các hàm:- sendNotification(userId, message) → ghi vào Firestore collection 'notifications'- getNotifications(userId) → lấy danh sách thông báo- markAsRead(notificationId) → đánh dấu đã đọc"
Khi nào thì tạo component mới, khi nào viết thẳng trong page?
Tạo component riêng khi:
Code đó sẽ dùng lại ở 2 nơi trở lên
Khối giao diện phức tạp (nhiều hơn 30-40 dòng)
Muốn tách isolate logic (form, bảng, bộ lọc)
Viết thẳng trong page khi:
Code đơn giản, chỉ dùng 1 lần
Chỉ là vài dòng ghép components có sẵn
Khi không chắc → bắt đầu viết trong page, tách ra sau khi thấy cần tái sử dụng.
Tại sao tách services ra khỏi components?
Lý do 1 — Tái sử dụng: Nhiều components cùng cần gọi getTasks(). Nếu code nằm trong component, phải copy-paste.Lý do 2 — Dễ thay đổi: Nếu sau này chuyển từ Firebase sang Supabase, bạn chỉ sửa file service — không cần sửa bất kỳ component nào.Lý do 3 — Dễ test: Service có thể được test độc lập, không cần render giao diện.
Thư mục hooks/ khác gì services/?
Tiêu chí
services/
hooks/
Loại hàm
Hàm thuần túy, gọi Firebase
Hàm React, quản lý state giao diện
Ví dụ
getTasks() trả về data
useTasks() trả về data + loading + error
Dùng ở đâu
Bất cứ đâu
Chỉ trong React components
Thực tế: hooks gọi services. Hook useTasks() bên trong gọi service getTasks(), rồi bổ sung quản lý loading state, error handling, caching.
Claude Code có tự tổ chức thư mục đúng không?
Claude Code biết các quy ước tổ chức thư mục Next.js tiêu chuẩn. Khi bạn mô tả rõ tính năng, Claude Code sẽ tự tạo file ở đúng vị trí.Tuy nhiên, ban đầu nên yêu cầu Claude Code thiết lập cấu trúc thư mục trước:
"Tạo cấu trúc thư mục chuẩn cho dự án:- app/ cho pages- components/ chia theo ui/, layout/, tasks/, shared/- services/ cho Firebase- hooks/ cho custom hooks- types/ cho TypeScript types"
Sau đó, mỗi lần thêm tính năng, Claude Code sẽ đặt file vào đúng thư mục.
Điểm mấu chốt: Dự án Next.js được tổ chức theo nguyên tắc “mỗi nhóm lo một việc”: Pages lo bố cục trang, Components lo giao diện tái sử dụng, Services lo kết nối dữ liệu. Hiểu cấu trúc này giúp bạn mô tả yêu cầu cho Claude Code chính xác hơn — biết cần thêm component ở đâu, sửa service nào, tạo page mới ra sao.