# Bài 00 — Tổng quan dự án & kiến trúc
⬅️ Về mục lục | Bài tiếp theo ➡️
🎯 Mục tiêu
Sau bài này bạn sẽ:
- Hiểu website bán hàng gồm những phần nào và mỗi phần làm gì
- Vẽ được luồng dữ liệu từ lúc người dùng bấm chuột đến lúc dữ liệu hiện lên màn hình
- Biết dự án Lith Perfume có những màn hình nào, dữ liệu nào
Bài này không code. Hãy đọc chậm, hiểu kỹ. 10 phút ở đây tiết kiệm 5 giờ debug sau này.
📚 1. Một website bán hàng gồm 3 phần
Hãy tưởng tượng bạn vào một quán cà phê:
| Ở quán cà phê | Trong website | Trong dự án này |
|---|---|---|
| Khu vực khách ngồi — bàn ghế, menu, nơi bạn gọi món | Frontend — giao diện người dùng nhìn thấy và bấm vào | Next.js chạy ở localhost:3000 |
| Nhà bếp — nhận order, chế biến, trả món ra | Backend — nhận yêu cầu, xử lý, trả dữ liệu | Express chạy ở localhost:5000 |
| Kho nguyên liệu — nơi cất trữ mọi thứ | Database — nơi lưu dữ liệu lâu dài | MongoDB chạy ở localhost:27017 |
Ba phần này là 3 chương trình riêng biệt, chạy độc lập, nói chuyện với nhau qua mạng.
💡 Điều quan trọng nhất cần nhớ: khi phát triển, bạn phải mở 3 thứ cùng lúc: MongoDB đang chạy, backend đang chạy, frontend đang chạy. Thiếu một cái là web sẽ lỗi.
📚 2. Frontend và Backend nói chuyện với nhau thế nào?
Chúng nói chuyện qua HTTP — cùng một giao thức mà trình duyệt dùng để mở trang web.
Một cuộc hội thoại mẫu
Người dùng mở trang chủ. Frontend cần danh sách sản phẩm, nên nó "hỏi" backend:
Frontend gửi: GET http://localhost:5000/products
Backend nhận được, đi hỏi MongoDB, rồi "trả lời" bằng JSON:
[
{
"_id": "6675905ecb0dded448a58bb0",
"name": "Gucci Flora Gorgeous Magnolia",
"image": "sp1.jpg",
"price": 5050000,
"rating": 4.5,
"categoryId": "669a978e6e282b058a8e3f2c"
},
{ "...": "sản phẩm tiếp theo" }
]
Frontend nhận mảng JSON này rồi vẽ ra các thẻ sản phẩm trên màn hình.
Bốn "động từ" HTTP bạn sẽ dùng
| Động từ | Ý nghĩa | Ví dụ trong dự án |
|---|---|---|
GET |
Lấy dữ liệu về | GET /products — lấy danh sách sản phẩm |
POST |
Tạo mới dữ liệu | POST /orders — tạo đơn hàng mới |
PUT / PATCH |
Sửa dữ liệu đã có | PUT /products/:id — sửa sản phẩm (bài 20) |
DELETE |
Xóa dữ liệu | DELETE /products/:id — xóa sản phẩm (bài 20) |
Dự án gốc chỉ có GET và POST. Bài 20 sẽ bổ sung PUT và DELETE.
Mã trạng thái (status code)
Backend luôn kèm theo một con số cho biết kết quả:
| Mã | Nghĩa | Khi nào gặp |
|---|---|---|
200 |
OK — thành công | Lấy dữ liệu thành công |
201 |
Created — đã tạo | Tạo đơn hàng thành công (chuẩn hơn 200) |
400 |
Bad Request — dữ liệu bạn gửi sai | Thiếu tên sản phẩm khi thêm |
401 |
Unauthorized — chưa đăng nhập | Vào /admin mà chưa login (bài 19) |
404 |
Not Found — không tìm thấy | Gọi URL không tồn tại |
500 |
Internal Server Error — backend lỗi | Code backend crash, sai cú pháp MongoDB |
📚 3. Kiến trúc dự án Lith Perfume
┌──────────────────────────────────────────────────────────────┐
│ TRÌNH DUYỆT (Chrome) │
│ │
│ ┌────────────────────────────────────────────────────────┐ │
│ │ FRONTEND — Next.js 14 http://localhost:3000 │ │
│ │ │ │
│ │ app/(user)/ app/admin/ │ │
│ │ ├ page.js (trang chủ) ├ page.js (dashboard) │ │
│ │ ├ menu/ └ product/ (quản lý SP) │ │
│ │ ├ product/[id]/ │ │
│ │ └ cart/ │ │
│ │ │ │
│ │ redux/ ← giỏ hàng, trạng thái sắp xếp, bộ lọc │ │
│ │ (lưu trong localStorage của trình duyệt) │ │
│ └────────────────────────────────────────────────────────┘ │
└──────────────────────────────────────────────────────────────┘
│
│ HTTP + JSON
│ (fetch / SWR)
▼
┌──────────────────────────────────────────────────────────────┐
│ BACKEND — Express http://localhost:5000 │
│ │
│ app.js ← cấu hình, bật CORS, gắn các router │
│ │ │
│ ├─ /products → routes/products.js │
│ ├─ /categories → routes/categories.js │
│ └─ /orders → routes/oder.js │
│ │
│ public/img/ ← ảnh sản phẩm (phục vụ tĩnh) │
│ model/db.js ← hàm kết nối MongoDB │
└──────────────────────────────────────────────────────────────┘
│
│ MongoDB Wire Protocol
▼
┌──────────────────────────────────────────────────────────────┐
│ DATABASE — MongoDB mongodb://localhost:27017 │
│ │
│ Database: lith_perfume │
│ ├─ collection "products" (12 sản phẩm) │
│ ├─ collection "categories" (10 thương hiệu) │
│ └─ collection "orders" (đơn hàng, ban đầu rỗng) │
└──────────────────────────────────────────────────────────────┘
📚 4. Theo dấu một hành động thật: "Thêm vào giỏ hàng"
Đây là ví dụ giúp bạn thấy toàn bộ hệ thống phối hợp. Hãy đọc kỹ từng bước.
Bối cảnh: người dùng đang ở trang chi tiết sản phẩm /product/6675905e...
Bước 1 — Frontend hiển thị sản phẩm
Khi trang vừa mở, component Productdetail chạy:
const { data: product } = useSWR(
`${process.env.NEXT_PUBLIC_API_URL}/products/id/${params.id}`,
fetcher
);
→ Trình duyệt gửi GET http://localhost:5000/products/id/6675905e...
Bước 2 — Backend nhận yêu cầu
app.js thấy URL bắt đầu bằng /products → chuyển cho routes/products.js.
Router thấy đường dẫn còn lại là /id/:id → chạy hàm xử lý tương ứng.
Bước 3 — Backend hỏi MongoDB
const products = await productCollection.findOne({
_id: new ObjectId(req.params.id)
});
MongoDB trả về một document sản phẩm.
Bước 4 — Backend "ghép" thêm danh mục
Sản phẩm chỉ lưu categoryId, không lưu tên thương hiệu. Backend đi tìm thêm:
const categories = await categoriesCollection.findOne({
_id: new ObjectId(products.categoryId)
});
products.category = categories; // gắn thêm vào
Rồi trả về JSON có đủ cả sản phẩm lẫn danh mục.
Bước 5 — Frontend vẽ ra màn hình
React nhận product, render tên, giá, ảnh, tên thương hiệu.
Bước 6 — Người dùng bấm "Thêm vào giỏ hàng"
Bước này KHÔNG gọi backend. Giỏ hàng nằm hoàn toàn ở phía trình duyệt:
dispatch(addItem({ product, quantity, size }));
→ Redux cập nhật mảng cart trong bộ nhớ
→ redux-persist tự động ghi mảng đó vào localStorage
→ Navbar (đang lắng nghe state.cart) tự động hiện số lượng mới
💡 Vì sao giỏ hàng không lưu ở server? Vì dự án chưa có đăng nhập — không biết giỏ hàng này là của ai. Lưu ở trình duyệt là cách đơn giản và đủ dùng. Bài 19 sẽ bàn về việc chuyển sang lưu theo tài khoản.
Bước 7 — Người dùng bấm "Đặt hàng"
Lúc này mới gọi backend:
fetch(`${process.env.NEXT_PUBLIC_API_URL}/orders`, {
method: 'POST',
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
user: { fullname, phone, address },
detail: cart,
total_money: total,
})
})
Backend chèn vào collection orders, trả về kết quả, frontend xóa giỏ hàng và chuyển về trang chủ.
📚 5. Ba collection trong database
categories — thương hiệu nước hoa
{
"_id": ObjectId("669a978e6e282b058a8e3f2c"),
"name": "Gucci",
"image": "h8.png"
}
10 bản ghi: Valentino, Chloé, Jean Paul Gaultier, Versace, Ralph Lauren, Burberry, Narciso Rodriguez, Gucci, Tom Ford, Kilian.
products — sản phẩm
{
"_id": ObjectId("6675905ecb0dded448a58bb0"),
"name": "Gucci Flora Gorgeous Magnolia",
"image": "sp1.jpg",
"price": 5050000,
"description": "Hương đầu: Quả mâm xôi, Dừa\n\nHương giữa: ...",
"rating": 4.5,
"categoryId": "669a978e6e282b058a8e3f2c",
"size": "100ml"
}
categoryId là "chìa khóa" trỏ sang collection categories. Đây gọi là quan hệ tham chiếu.
⚠️ Cảnh báo sớm — sẽ giải thích kỹ ở bài 06: Trong file dữ liệu mẫu
products.txt,categoryIdlà chuỗi ("669a978e..."), nhưng_idbêncategorieslại là ObjectId. Hai kiểu này không bằng nhau trong MongoDB. Đây là nguyên nhân khiến route lọc theo danh mục trong source gốc trả về mảng rỗng. Bài 06 sẽ chỉ rõ cách sửa.
orders — đơn hàng
{
"_id": ObjectId("..."),
"user": {
"fullname": "Nguyễn Văn A",
"phone": "0901234567",
"address": "123 Lê Lợi, Q1, TP.HCM"
},
"detail": [
{ "_id": "...", "name": "Gucci Flora...", "price": 5050000, "quantity": 2, "size": "s" }
],
"total_money": 10100000
}
Toàn bộ giỏ hàng được "chụp lại" vào trường detail. Đây là cách làm phổ biến và đúng đắn:
nếu sau này giá sản phẩm thay đổi, đơn hàng cũ vẫn giữ nguyên giá lúc mua.
📚 6. Bản đồ các màn hình
Khu khách hàng
| URL | File | Chức năng |
|---|---|---|
/ |
app/(user)/page.js |
Trang chủ: slider, logo thương hiệu, sản phẩm nổi bật |
/menu |
app/(user)/menu/page.js |
Tất cả sản phẩm + sidebar lọc/sắp xếp |
/menu/[id] |
app/(user)/menu/[id]/page.js |
Sản phẩm theo một danh mục |
/product/[id] |
app/(user)/product/[id]/page.js |
Chi tiết sản phẩm |
/cart |
app/(user)/cart/page.js |
Giỏ hàng + đặt hàng |
/login |
app/(user)/login/page.js |
Form đăng nhập (bài 19 nối API) |
/register |
app/(user)/register/page.js |
Form đăng ký (bài 19 nối API) |
Khu quản trị
| URL | File | Chức năng |
|---|---|---|
/admin |
app/admin/page.js |
Dashboard thống kê |
/admin/product |
app/admin/product/page.js |
Bảng danh sách sản phẩm |
/admin/product/add |
app/admin/product/add/page.js |
Form thêm sản phẩm + upload ảnh |
📚 7. Danh sách API đầy đủ
Đây là "hợp đồng" giữa frontend và backend. Bạn sẽ xây từng cái ở bài 05–08.
| Method | Đường dẫn | Trả về | Bài |
|---|---|---|---|
GET |
/categories |
Mảng 10 danh mục | 05 |
GET |
/products |
Mảng sản phẩm, mỗi cái kèm category |
06 |
GET |
/products/id/:id |
1 sản phẩm kèm category |
06 |
GET |
/products/byCategory/:id |
Sản phẩm thuộc 1 danh mục | 06 |
GET |
/products/topRating |
Top 10 sản phẩm đánh giá cao | 06 |
POST |
/products |
Thêm sản phẩm (có upload ảnh) | 07 |
GET |
/orders |
Mảng đơn hàng | 08 |
POST |
/orders |
Tạo đơn hàng mới | 08 |
Ngoài ra backend còn phục vụ file tĩnh: GET /img/sp1.jpg → trả về ảnh trong backend/public/img/.
⚠️ Những điểm chưa hoàn thiện của source gốc
Tài liệu này dạy bạn dựng lại dự án, đồng thời chỉ ra chỗ chưa chuẩn. Đây là danh sách tổng hợp — mỗi mục sẽ được mổ xẻ ở bài tương ứng:
| Vấn đề | Bài xử lý |
|---|---|
| Backend mặc định chạy port 3000, trùng với Next.js → phải đổi sang 5000 | 04 |
routes/index.js viết xong nhưng không được gắn vào app.js → code chết |
04 |
routes/oder.js bị copy nguyên từ products.js, có các route vô nghĩa (topRating, byCategory cho đơn hàng) |
08 |
categoryId là chuỗi trong dữ liệu mẫu nhưng code truy vấn bằng ObjectId → lọc theo danh mục ra rỗng |
06 |
Không có file .env.local trong repo, nhưng code phụ thuộc vào NEXT_PUBLIC_API_URL |
12 |
Productlist dùng useRef chụp dữ liệu lần đầu → nút "Mặc định" hiển thị sai khi đổi danh mục |
15 |
Productlist gọi .sort() trực tiếp trên mảng của props → biến đổi dữ liệu gốc |
15 |
| Trang chi tiết sản phẩm có 2 nút "Thêm vào giỏ hàng" trùng nhau | 11 |
app/admin/layout.js viết sai chính tả "use clinet" (đúng: "use client") |
17 |
app/admin/layout.js có thẻ <html>/<body> lồng trong layout gốc → HTML không hợp lệ |
17 |
app/(user)/product/page.js là component rỗng, không return gì → lỗi khi vào /product |
10 |
recentOrder.js gọi toLOcaString() (sai chính tả) → crash nếu dùng |
17 |
| Form đăng nhập/đăng ký chỉ là HTML tĩnh, chưa nối API | 19 |
| Ô tìm kiếm trên navbar chưa hoạt động | 21 |
Công thức tính tiền theo size ở trang giỏ hàng không nhất quán (size l cộng theo số lượng, size m cộng cố định) |
16 |
| Không có validate dữ liệu ở backend — gửi gì cũng nhận | 07 |
📌 Đừng lo lắng. Có lỗi là chuyện bình thường của một dự án học tập. Việc bạn nhìn ra được lỗi mới là điều quan trọng — đó là kỹ năng của lập trình viên thật.
✅ Kiểm tra hiểu bài
Trả lời được 5 câu này thì bạn đã sẵn sàng sang bài 01:
- Khi mở trang chủ, dữ liệu sản phẩm đi qua bao nhiêu chương trình? Kể tên.
- Khi bấm "Thêm vào giỏ hàng", backend có được gọi không? Vì sao?
categoryIdtrongproductsdùng để làm gì?- Nếu quên bật MongoDB, backend sẽ báo lỗi ở đâu?
- Vì sao đơn hàng lại lưu bản sao thông tin sản phẩm chứ không chỉ lưu
productId?
- Ba: trình duyệt (Next.js) → Express → MongoDB, rồi ngược lại.
- Không. Giỏ hàng lưu ở Redux + localStorage trong trình duyệt. Chỉ khi bấm "Đặt hàng" mới gọi
POST /orders. - Trỏ sang
_idcủa một document trong collectioncategories, để biết sản phẩm thuộc thương hiệu nào. - Ở hàm
connectDb()trongmodel/db.js— lệnhclient.connect()sẽ ném lỗi và request bị treo/500. - Để "đóng băng" giá và thông tin tại thời điểm mua. Nếu sau này đổi giá sản phẩm, đơn hàng cũ vẫn đúng.
All rights reserved