
Clean Architecture trong Node.js: Nguyên tắc và folder structure chuẩn
Clean Architecture (Kiến trúc sạch) là một phong cách thiết kế phần mềm được Robert C. Martin (Uncle Bob) đề xuất để tạo ra các hệ thống dễ bảo trì, dễ kiểm thử và độc lập với framework. Trong môi trường Node.js nơi các framework như Express, Koa, Fastify thay đổi nhanh chóng, áp dụng Clean Architecture giúp tách biệt logic kinh doanh khỏi chi tiết triển khai, khiến ứng dụng trở nên linh hoạt và bền bỉ hơn.
Bài viết này giải thích nguyên tắc Core của Clean Architecture và hướng dẫn cách áp dụng nó vào dự án Node.js thực tế, bao gồm cấu trúc thư mục, ví dụ mã nguồn và lợi ích mà nó mang lại.
Nguyên tắc cơ bản của Clean Architecture
Clean Architecture dựa trên nguyên tắc Dependency Rule: các vòng trong vòng tròn chỉ có thể phụ thuộc vào vòng trong ngay bên trong. Điều này đồng nghĩa với: logic kinh doanh (business logic) không biết tới bất kỳ framework, database hoặc công cụ bên ngoài nào.
Các vòng từ trong ra ngoài:
- Entities (Đối tượng nghiệp vụ): Các đối tượng encapsulates nghiệp vụ rộng nhất có thể. Có thể là đối tượng nghiệp vụ chung được sử dụng bởi nhiều ứng dụng khác nhau trong cùng một doanh nghiệp.
- Use Cases (Các trường hợp sử dụng): Chứa logic nghiệp vụ cụ thể của ứng dụng. Các use case orchestrates luồng dữ liệu tới và từ các entity, và điều phối sử dụng các entity để đạt được mục đích nghiệp vụ.
- Interface Adapters (Adapters giao diện): Chuyển đổi dữ liệu từ dạng thuận tiện nhất cho use cases và entities sang dạng thuận tiện nhất cho bên ngoài như database, web, hoặc UI. Ở đây là nơi có các presenters, views, và controllers.
- Frameworks & Drivers (Framework và thiết bị điều khiển): Vòng ngoài nhất, chứa tất cả các công cụ chi tiết như web framework, database, UI, etc. Đây là nơi bạn đặt các file như routes, database migrations, và cấu hình server.

Áp dụng Clean Architecture vào Node.js
Trong môi trường Node.js, chúng ta thường thấy các dự án với cấu trúc như:
project/ ├── src/ │ ├── controllers/ │ ├── models/ │ ├── routes/ │ ├── services/ │ └── utils/ ├── tests/ └── package.json
Cấu trúc này thường dẫn đến strong coupling giữa các lớp – controllers biết về models, routes biết về controllers, etc. Khi thay đổi framework (từ Express sang Fastify) hoặc database (từ MongoDB sang PostgreSQL), cần sửa đổi rộng rãi ở nhiều nơi.
Với Clean Architecture, chúng ta có thể tái cấu trúc thành:
src/ ├── domain/ # Entities và Use Cases │ ├── entities/ │ │ ├── User.js │ │ └── Order.js │ └── use-cases/ │ ├── CreateUser.js │ ├── GetUser.js │ └── CreateOrder.js ├── application/ # Interface Adapters │ ├── controllers/ │ │ ├── UserController.js │ │ └── OrderController.js │ ├── presenters/ │ └── dtos/ ├── infrastructure/ # Framework & Drivers │ ├── database/ │ │ ├── mongo/ │ │ └── postgres/ │ ├── web/ │ │ ├── express/ │ │ │ ├── routes/ │ │ │ └── middleware/ │ │ └── fastify/ │ └── config/ └── interfaces/ # Giao diện giữa các lớp (optional)

Ví dụ thực tế: Use Case và Controller
Hãy xem xét một use case đơn giản: tạo người dùng mới.
1. Entity (domain/entities/User.js)
class User {
constructor(id, name, email, createdAt) {
this.id = id;
this.name = name;
this.email = email;
this.createdAt = createdAt || new Date();
}
}
module.exports = User;
2. Use Case (domain/use-cases/CreateUser.js)
const User = require('../entities/User');
class CreateUser {
constructor(userRepository) {
this.userRepository = userRepository;
}
async execute(name, email) {
// Validate input
if (!name || !email) {
throw new Error('Name and email are required');
}
// Check if email already exists
const existing = await userRepository.findByEmail(email);
if (existing) {
throw new Error('Email already in use');
}
// Create new user entity
const user = new User(
null, // ID sẽ được DB tạo
name,
email
);
// Save to repository
const savedUser = await userRepository.save(user);
return savedUser;
}
}
module.exports = CreateUser;
3. Controller (application/controllers/UserController.js)
const CreateUser = require('../../domain/use-cases/CreateUser');
class UserController {
constructor(createUserUseCase) {
this.createUserUseCase = createUserUseCase;
}
async handleRequest(req, res) {
try {
const { name, email } = req.body;
const user = await this.createUserUseCase.execute(name, email);
res.status(201).json({
success: true,
data: user
});
} catch (error) {
res.status(400).json({
success: false,
error: error.message
});
}
}
}
module.exports = UserController;
Lưu ý: Controller không biết gì về Express – nó chỉ nhận request dưới dạng object thuần túy và trả về response dưới dạng object. Điều này làm cho việc thay đổi web framework trở nên cực kỳ đơn giản.
Lợi ích của Clean Architecture trong Node.js
1. Dễ kiểm thử (Testable)
Vì use cases và entities không phụ thuộc vào framework, bạn có thể kiểm thử chúng đơn giản bằng các mock object đơn giản – không cần khởi động web server hoặc database.
2. Độc lập với framework (Framework Independent)
Bạn có thể thay đổi từ Express sang Fastify, Koa hoặc nawet HTTP server thuần túy mà không cần sửa đổi một dòng mã trong domain layer hoặc application layer.
3. Dễ bảo trì (Maintainable)
Mỗi lớp có trách nhiệm rõ ràng. Khi cần thay đổi logic kinh doanh, bạn chỉ cần sửa trong use case layer. Khi cần thay đổi cách lưu trữ, bạn chỉ cần sửa trong infrastructure layer.
4. Tiết kiệm thời gian trong dài hạn
Mặc dù cấu trúc ban đầu có thể phức tạp hơn sedikit, nhưng trong dài hạn, nó giảm đáng kể thời gian cần để sửa lỗi, thêm tính năng mới và thích ứng với thay đổi công nghệ.
Thực tiễn áp dụng
Để bắt đầu với Clean Architecture trong Node.js:
- Xác định Entities: Những đối tượng nghiệp vụ cốt lõi của ứng dụng là gì?
- Xác định Use Cases: Các thao tác nghiệp vụ mà ứng dụng cần hỗ trợ?
- Tạo Repository interfaces: Định nghĩa các interface như UserRepository, OrderRepository mà use cases sẽ sử dụng.
- Implement Infrastructure: Tạo các triển khai thực tế của repository interfaces cho database cụ thể bạnเลือก.
- Kết nối qua Dependency Injection: Sử dụng một container đơn giản hoặc truyền thủ công để cung cấp triển khai thực tế cho use cases.
Nhiều thư viện Node.js hỗ trợ mô hình này như Awilix (dependency injection container) або bạn có thể chỉ dùng ES6 modules và truyền thủ công.
Kết luận
Clean Architecture không phải là một silver bullet, nhưng nó cung cấp một mô hình suy nghĩ vững chắc để xây dựng hệ thống Node.js bền bỉ. Khi ứng dụng của bạn phát triển và yêu cầu thay đổi tăng lên, việc đầu tiên vào kiến trúc sạch sẽ trả lời lại nhiều gấp lần trong términos của thời gian phát triển và giảm nguy cơ lỗi.
Nếu bạn đang bắt đầu một dự án Node.js mới hoặc cân nhắc tái cấu trúc một dự án hiện tại, hãy thử áp dụng Clean Architecture – kết quả sẽ khiến bạn ngạc nhiên về mức độ dễ làm việc với mã nguồn trong dài hạn.
Nguồn tham khảo: Clean Architecture by Robert C. Martin | Awilix – DI Container for Node.js | Node.js Documentation
