1. Mục tiêu của kiến trúc
Khi xây dựng một ứng dụng React/Next.js, chúng ta không chỉ viết code để "chạy được", mà cần một cấu trúc bền vững để:
- Tách biệt rõ ràng: UI – Logic – Business – Data.
- Khả năng mở rộng (Scalability): Dễ dàng thêm tính năng mới mà không làm vỡ tính năng cũ.
- Dễ bảo trì (Maintainability): Code dễ đọc, dễ test, dễ onboarding cho người mới.
- Tránh Over-engineering: Cung cấp giải pháp phù hợp cho từng quy mô dự án (từ Admin Tool nhỏ đến SaaS Enterprise).
Kiến trúc này phù hợp cho:
- Các hệ thống CRUD Admin, Dashboard.
- Hệ thống CRM / ERP phức tạp.
- Các ứng dụng SaaS có nhiều luồng (flow) và phân quyền (role) phức tạp.
2. Nguyên tắc cốt lõi
2.1. Single Responsibility Principle (Mỗi file một việc)
Một file không nên làm quá nhiều thứ.
- ❌ Bad: Một component vừa render UI, vừa gọi API
fetch('/api/...'), vừa xử lý logicif (role === 'admin'). Đây là "mùi code" (code smell). - ✅ Good: Tách riêng:
UI Component: Chỉ render.API Service: Chỉ gọi data.Hook/Presenter: Kết nối và xử lý logic.
2.2. Phân biệt 4 loại code
Hiểu rõ bản chất từng dòng code bạn viết thuộc loại nào:
| Loại Code | Mô tả | Trách nhiệm | Ví dụ |
|---|---|---|---|
| UI (Presentation) | Giao diện | Chỉ lo việc hiển thị, bố cục (Layout). | Button, Table, Form, Modal |
| UI Logic | Điều phối | Kết nối Data vào UI, xử lý trạng thái hiển thị (loading, modal open). | useProductList, ProductPresenter |
| Business Logic | Nghiệp vụ | Các quy tắc cốt lõi của ứng dụng, không phụ thuộc UI. | canPublishProduct(), calculateDiscount() |
| Infrastructure | Hạ tầng | Làm việc với bên ngoài (API, Storage, 3rd party). | apiClient, localStorageService |
2.3. UI phải "Dumb" (Ngây ngô)
UI Component (Presentation Layer) không nên biết về logic nghiệp vụ phức tạp.
- KHÔNG tự check quyền:
if (user.role === 'ADMIN') return <Button />. - KHÔNG tự gọi API:
useEffect(() => fetch(...)). - CHỈ nhận props đơn giản:
label,disabled,hidden,onClick.
// ✅ Good UI: Chỉ nhận kết quả cuối cùng từ logic
<Button
disabled={shouldDisableButton} // True/False đã được tính toán ở ngoài
onClick={handleSave}
>
Lưu sản phẩm
</Button>3. Các Layer và Trách nhiệm
Mô hình dữ liệu chảy: UI Component → Presenter (Hook) → Use Case (Service) → Domain Rules → Infra/Data
3.1. UI Layer (Presentation)
- Trách nhiệm: Render HTML/CSS, nhận user interaction, emit event.
- Không được: Gọi API trực tiếp, viết logic if/else phức tạp.
3.2. Presenter / ViewModel Layer (The Glue)
- Trách nhiệm:
- Gom nhặt State từ nhiều nguồn (URL, Server Data, User Input).
- Gọi Domain Rules để quyết định UI nên hiển thị thế nào.
- Trả về một object "ViewModel" đơn giản cho UI tiêu thụ.
- Đặc điểm: Thường là Custom Hooks (
useMyFeature).
// useProductViewModel.ts
function useProductViewModel(product, user) {
// Logic quyết định hiển thị
const canEdit = product.ownerId === user.id;
const showAuditLog = user.role === 'ADMIN';
return {
actions: {
edit: { disabled: !canEdit, label: canEdit ? 'Edit' : 'Read Only' }
},
ui: { showAuditLog }
};
}3.3. Application Layer (Use Cases)
- Trách nhiệm: Điều phối flow nghiệp vụ.
- "Khi user bấm nút Save -> Validate dữ liệu -> Gọi API -> Gửi Analytics Log -> Hiện thông báo thành công".
- Đặc điểm: Framework-agnostic (cố gắng ít phụ thuộc vào React nhất có thể).
3.4. Domain Layer (Core)
- Trách nhiệm: Chứa các "Business Laws" bất biến.
- Đặc điểm: Pure Functions, dễ test nhất.
// produc.rules.ts
export function isProductPublishable(product: Product): boolean {
return product.price > 0 && product.images.length >= 1;
}3.5. Infrastructure Layer
- Trách nhiệm: Gọi API, lưu LocalStorage, tương tác với 3rd party SDK.
4. Cấu trúc thư mục (Feature-First)
Thay vì chia theo components/, hooks/, utils/, hãy chia theo Feature để dễ mở rộng (Colocation).
src/
├── features/ # Các module lớn của ứng dụng
│ ├── products/
│ │ ├── components/ # Dumb UI specific to this feature
│ │ │ ├── ProductCard.tsx
│ │ │ └── ProductForm.tsx
│ │ ├── hooks/ # Presenters / Logic
│ │ │ └── useProductList.ts
│ │ ├── domain/ # Business Rules (Pure TS)
│ │ │ └── product.rules.ts
│ │ ├── api/ # Data fetching specific to this feature
│ │ │ └── product.api.ts
│ │ └── index.ts # Public API của feature này
│ │
│ └── auth/ ...
│
├── shared/ # Dùng chung cho toàn app
│ ├── ui/ # Button, Input, Modal (Design System)
│ ├── hooks/ # useToggle, useDebounce
│ └── utils/ # formatting, date
│
└── app/ (Next.js pages) # Chỉ import features và render4.2. So sánh các cách tổ chức thư mục
Tại sao chọn Feature-based thay vì các cách khác?
| Loại hình | Structure Pattern | Ưu điểm | Nhược điểm |
|---|---|---|---|
| Layer-First | src/components, src/hooks, src/utils | Quen thuộc, dễ bắt đầu. | Khi app lớn, các file liên quan nằm rải rác. Khó xóa hoặc refactor feature. |
| Feature-First (Khuyên dùng) | src/features/products, src/features/auth | Colocation: Mọi thứ liên quan nằm chung 1 chỗ. Dễ scale, dễ xóa feature. | Cần kỷ luật để import chéo (Feature A không được import sâu Feature B). |
| Atomic Design | atoms, molecules, organisms | Tái sử dụng UI cực tốt. | Chỉ giải quyết vấn đề UI, không giải quyết logic/domain. Dễ gây tranh cãi "cái này là mole hay org?". |
Lời khuyên: Kết hợp Feature-First cho logic/nghiệp vụ và Atomic/Layer cho thư mục
shared/ui(Design System).
4.3. Deep Dive: Case Study Phức Tạp (Order Management)
Giả sử ta xây dựng module Quản lý Đơn hàng (Orders) phức tạp trong một hệ thống ERP. Yêu cầu:
- Hiển thị danh sách đơn hàng.
- Có bộ lọc phức tạp (Date, Status, Customer Type).
- Tính toán tổng tiền, chiết khấu (Business Rules).
- Quyền: Chỉ Manager mới được duyệt đơn.
Cấu trúc file thực tế sẽ trông như thế này:
src/features/orders/
├── api/
│ ├── orders.api.ts # createOrder, fetchOrderById
│ └── orders.dto.ts # Interfaces API Response (Backend DTO)
│
├── domain/ # 🧠 LOGIC TRÁI TIM (Pure TS)
│ ├── order.entity.ts # Order Type (Frontend Model)
│ ├── order.rules.ts # isOrderReturnable, calculateDiscount
│ └── order.status.ts # Constants & Enums
│
├── components/ # 🧱 DUMB UI (Presentation Only)
│ ├── OrderListTable.tsx # Chỉ nhận props data, onAction
│ ├── OrderStatusBadge.tsx
│ └── OrderFilterForm.tsx
│
├── hooks/ # 🔌 COMPOSITION Logic
│ ├── useOrderList.ts # Gọi API, logic Filter, Pagination
│ └── useOrderDetail.ts # Gọi API detail, Mappers
│
├── containers/ (Optional) # 🎬 SMART COMPONENTS (Nối Hook vào UI)
│ └── OrderListContainer.tsx # Dùng useOrderList -> render OrderListTable
│
└── index.ts # 🚪 PUBLIC APIPhân tích luồng dữ liệu (Data Flow):
-
Container (
OrderListContainer.tsx):- Là "Smart Component".
- Gọi Hook:
const { orders, isLoading, approveOrder } = useOrderList(). - Render:
<OrderListTable data={orders} onApprove={approveOrder} />.
-
UI (
OrderListTable.tsx):- Là "Dumb Component".
- Chỉ nhận props và hiển thị. Không gọi API, không dùng hook logic.
-
Presenter (
useOrderList.ts):- Gọi API
fetchOrders. - Gọi Domain Rules
isOrderApprovable. - Trả về data đã được xử lý cho Container.
- Gọi API
-
Domain (
order.rules.ts):- Business Logic thuần túy (Pure TS).
Khi nào cần thư mục containers/?
- Dự án nhỏ: Container chính là
app/orders/page.tsx. Không cần foldercontainersriêng. - Dự án lớn: Feature phức tạp có thể được nhúng vào nhiều nơi (ví dụ: Danh sách đơn hàng hiện ở Dashboard VÀ ở trang Customer Detail). Khi đó ta cần
containers/OrderListContainer.tsxđể tái sử dụng cả Logic + UI ở nhiều chỗ khác nhau.
4.4. Vai trò của thư mục app/ (Next.js App Router)
Bạn sẽ thắc mắc: "Nếu logic nằm hết ở features, thì thư mục app dùng để làm gì?"
Trong kiến trúc này, app/ đóng vai trò là Router & Orchestrator. Nó nên càng "mỏng" (thin) càng tốt.
Trách nhiệm chính:
- Routing: Định nghĩa URL (
/products,/settings). - Metadata (SEO): Định nghĩa
title,description. - Server Data Fetching: (Nếu dùng Server Components) Fetch data tầng server để pass xuống Client Component.
- Layout Composition: Lồng ghép Feature vào Layout chung (Sidebar, Header).
Ví dụ file src/app/orders/page.tsx (Rất gọn):
import { OrderListContainer } from '@/features/orders'; // Import từ Public API
// 1. SEO
export const metadata = {
title: 'Quản lý đơn hàng | ERP System',
};
export default async function OrdersPage() {
// 2. (Optional) Server-side Data Fetching
// const initialData = await fetchOrdersServerSide();
return (
<main className="p-6">
{/* 3. Render Feature Container */}
<OrderListContainer />
</main>
);
}➡️ Lợi ích: Nếu sau này bạn chuyển từ Next.js sang Vite (React Router), bạn chỉ cần thay thư mục app/. Toàn bộ logic trong features/ giữ nguyên 100%.
5. Chiến lược theo quy mô (Scale)
Không phải dự án nào cũng cần chia 5 lớp. Hãy áp dụng linh hoạt.
5.1. Level 1: Dự án nhỏ / Prototype
- UI + API gọi trực tiếp trong Component (hoặc React Query hook đơn giản).
- File:
ProductPage.tsxchứa mọi thứ.
5.2. Level 2: Dự án trung bình (Production Ready)
- Tách Presenter (Custom Hook) để UI sạch sẽ.
- Tách Domain rules ra file riêng nếu logic lặp lại.
- Cấu trúc:
ui+hooks+api.
5.3. Level 3: Dự án lớn (Enterprise / Complex Logic)
- Áp dụng đầy đủ 4-5 layers.
- Bắt buộc Unit Test cho Domain và Use Cases.
- Cần chặt chẽ để nhiều team cùng làm việc (Feature A không được import trực tiếp file ruột của Feature B, chỉ qua
index.ts).
6. Checklist kiểm tra kiến trúc
Trước khi merge code, hãy tự hỏi:
- UI Component có chứa logic nghiệp vụ (
if role == ...) không? (Nếu có -> Move vào Presenter). - Presenter có chứa JSX không? (Nên hạn chế, chỉ return data).
- Domain Rules có phụ thuộc vào React không? (Tuyệt đối không, phải là pure TS).
7. Bonus: Prompting AI (System Instruction)
Đây là Prompt mẫu để bạn hướng dẫn AI (ChatGPT/Claude) code theo đúng architecture này:
"Act as a Senior React Engineer. I need you to build the [Feature Name] module following a Feature-First + DDD (Domain Driven Design) approach.
Architecture Rules:
- Folder Structure: Create a folder
src/features/[feature-name]with subfolders:api,domain,hooks(presenters),components(dumb UI), and optionallycontainers(smart components).- Domain Rules: Separation of concerns is key. Business logic (validation, permission, calculation) MUST be pure functions in
domain/*.rules.ts. No React code in domain.- UI Components: Must be DUMB. No
useEffect, nofetch. They only receive data/callbacks via props.- Presenter (Hooks): Create a custom hook
use[Feature]ViewModelto handle state, call API, check domain rules, and return a cleanViewModelfor the UI.- Containers: Use
containersto wire up the Hook/Presenter with the Dumb UI. This is what you export inindex.ts.- App Directory: The
src/appfolder is for Routing & Layouts only. It should strictly import the Container fromsrc/featuresand render it. No business logic inpage.tsx.- Styling: Use TailwindCSS, mobile-first.
Task: Implement the [Feature Name] with requirements: [List requirements...]"