Mục lục

Kiến trúc ứng dụng mở rộng (Scalable Architecture)

Nguyên tắc thiết kế hệ thống tách biệt UI, Logic, và Business. Mô hình 4 lớp (UI, Presenter, Use Case, Domain) giúp dự án dễ dàng mở rộng từ nhỏ đến lớn.

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 để:

  1. Tách biệt rõ ràng: UI – Logic – Business – Data.
  2. 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ũ.
  3. Dễ bảo trì (Maintainability): Code dễ đọc, dễ test, dễ onboarding cho người mới.
  4. 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ý logic if (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 CodeMô tảTrách nhiệmVí dụ
UI (Presentation)Giao diệnChỉ lo việc hiển thị, bố cục (Layout).Button, Table, Form, Modal
UI LogicĐiều phốiKết nối Data vào UI, xử lý trạng thái hiển thị (loading, modal open).useProductList, ProductPresenter
Business LogicNghiệp vụCác quy tắc cốt lõi của ứng dụng, không phụ thuộc UI.canPublishProduct(), calculateDiscount()
InfrastructureHạ tầngLà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.
tsx:
// ✅ 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 ComponentPresenter (Hook)Use Case (Service)Domain RulesInfra/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).
tsx:
// 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.
typescript:
// 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).

text:
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à render

4.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ìnhStructure PatternƯu điểmNhược điểm
Layer-Firstsrc/components, src/hooks, src/utilsQuen 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/authColocation: 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 Designatoms, molecules, organismsTá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:

text:
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 API

Phân tích luồng dữ liệu (Data Flow):

  1. Container (OrderListContainer.tsx):

    • Là "Smart Component".
    • Gọi Hook: const { orders, isLoading, approveOrder } = useOrderList().
    • Render: <OrderListTable data={orders} onApprove={approveOrder} />.
  2. 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.
  3. Presenter (useOrderList.ts):

    • Gọi API fetchOrders.
    • Gọi Domain Rules isOrderApprovable.
    • Trả về data đã được xử lý cho Container.
  4. 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 folder containers riê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:

  1. Routing: Định nghĩa URL (/products, /settings).
  2. Metadata (SEO): Định nghĩa title, description.
  3. Server Data Fetching: (Nếu dùng Server Components) Fetch data tầng server để pass xuống Client Component.
  4. 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):

tsx:
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.tsx chứ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:

  1. UI Component có chứa logic nghiệp vụ (if role == ...) không? (Nếu có -> Move vào Presenter).
  2. Presenter có chứa JSX không? (Nên hạn chế, chỉ return data).
  3. 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:

  1. Folder Structure: Create a folder src/features/[feature-name] with subfolders: api, domain, hooks (presenters), components (dumb UI), and optionally containers (smart components).
  2. 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.
  3. UI Components: Must be DUMB. No useEffect, no fetch. They only receive data/callbacks via props.
  4. Presenter (Hooks): Create a custom hook use[Feature]ViewModel to handle state, call API, check domain rules, and return a clean ViewModel for the UI.
  5. Containers: Use containers to wire up the Hook/Presenter with the Dumb UI. This is what you export in index.ts.
  6. App Directory: The src/app folder is for Routing & Layouts only. It should strictly import the Container from src/features and render it. No business logic in page.tsx.
  7. Styling: Use TailwindCSS, mobile-first.

Task: Implement the [Feature Name] with requirements: [List requirements...]"

Quảng cáo
mdhorizontal