0

Bài 01 — Tổng quan dự án Yotea & kiến trúc full-stack

Phần 0 · Khởi động — Thời lượng ước tính: ~30 phút ⬅️ Bài trước: Mục lục · Bài sau: 02 — Cài đặt môi trường và chạy dự án lần đầu ➡️ 🏠 Mục lục


🎯 Sau bài này bạn sẽ

  • Giải thích được full-stack nghĩa là gì và Yotea gồm những mảnh nào ghép lại.
  • Vẽ lại được sơ đồ đường đi của dữ liệu từ lúc người dùng bấm chuột đến lúc chữ hiện ra màn hình.
  • Biết file nào nằm ở đâuthư mục nào chịu trách nhiệm gì trong cả yotea-be lẫn yotea-fe.
  • Lần theo được một chức năng thật (hiển thị danh sách sản phẩm) qua đủ 7 lớp của hệ thống.
  • Đọc được tên 14 nhóm chức năng của dự án và biết mỗi nhóm giải quyết nghiệp vụ gì.

📋 Cần chuẩn bị

  • Một trình soạn thảo code, khuyến nghị Visual Studio Code.
  • Đã tải/clone dự án về máy (thư mục gốc chứa 2 thư mục con yotea-beyotea-fe).
  • Bài này chưa cần chạy được dự án — chỉ đọc và mở file. Việc cài đặt để ở Bài 02.

1. "Full-stack" thực ra là gì?

Hãy tưởng tượng một quán trà sữa ngoài đời:

Ngoài đời Trong lập trình web Trong dự án này
Quầy pha chế, menu treo tường, nhân viên order Frontend — thứ khách nhìn thấy và bấm vào yotea-fe
Bếp phía sau: nhận phiếu, kiểm tra nguyên liệu, quyết định làm hay từ chối Backend — nơi xử lý nghiệp vụ và bảo mật yotea-be
Kho nguyên liệu, sổ ghi đơn hàng Database — nơi lưu dữ liệu lâu dài MongoDB
Phiếu order chạy qua lại giữa quầy và bếp HTTP + JSON — giao thức truyền tin Các request http://localhost:8080/api/...

Khách hàng không bao giờ được vào bếp. Muốn gì cũng phải qua nhân viên, viết phiếu, đưa vào trong. Đó chính là lý do frontend không được nói chuyện thẳng với database — mọi thứ phải đi qua backend để backend còn kiểm tra "bạn là ai, bạn có quyền không".

📖 Thuật ngữ: full-stack = làm cả "tầng trên" (frontend) lẫn "tầng dưới" (backend + database). Học full-stack nghĩa là bạn tự làm được trọn một sản phẩm.

Ba tiến trình chạy song song

Khi dự án Yotea hoạt động, trên máy bạn có ba chương trình chạy cùng lúc:

┌───────────────────────────┐         ┌───────────────────────────┐         ┌──────────────────┐
│      TRÌNH DUYỆT          │         │      NODE.JS              │         │    MONGODB       │
│  ─────────────────────    │         │  ─────────────────────    │         │  ──────────────  │
│      yotea-fe             │ ──────▶ │      yotea-be             │ ──────▶ │   database       │
│  React 18 · Redux         │  HTTP   │  Express · Mongoose       │  driver │   "yotea"        │
│                           │  JSON   │                           │         │                  │
│  localhost:3000           │ ◀────── │  localhost:8080           │ ◀────── │  localhost:27017 │
└───────────────────────────┘         └───────────────────────────┘         └──────────────────┘
        Giao diện                          Nghiệp vụ + bảo mật                    Lưu trữ
     (chạy trên máy khách)                  (chạy trên server)                 (chạy trên server)

Thiếu một trong ba, web sẽ hỏng theo kiểu khác nhau:

Tắt cái gì Triệu chứng bạn sẽ thấy
Tắt MongoDB Backend in lỗi MongooseServerSelectionError, mọi API trả lỗi
Tắt backend Web mở được nhưng trắng trơn, Console báo ERR_CONNECTION_REFUSED
Tắt frontend localhost:3000 không vào được, nhưng gọi API bằng Postman vẫn chạy

2. Soi cấu trúc thư mục thật

2.1. Nhìn từ trên xuống

PROJECT-SELL-MILK-TEA/
├── yotea-be/          ← Backend: API + database
│   ├── .babelrc
│   ├── package.json
│   └── src/
│       ├── app.js           ← điểm khởi động của backend
│       ├── routes/          ← 14 file: khai báo đường dẫn URL
│       ├── controllers/     ← 14 file: xử lý logic cho từng đường dẫn
│       ├── models/          ← 14 file: mô tả cấu trúc dữ liệu trong MongoDB
│       └── middlewares/     ← 1 file: kiểm tra đăng nhập & phân quyền
│
└── yotea-fe/          ← Frontend: giao diện người dùng
    ├── package.json
    ├── tailwind.config.js
    ├── public/
    └── src/
        ├── index.js         ← điểm khởi động của frontend
        ├── App.js           ← khai báo toàn bộ đường dẫn (route) của web
        ├── api/             ← 15 file: các hàm gọi sang backend
        ├── redux/           ← 13 file: kho dữ liệu dùng chung
        ├── pages/           ← các trang: user, admin, auth, layouts
        ├── components/      ← các mảnh giao diện dùng lại nhiều nơi
        └── utils/           ← hàm tiện ích: định dạng tiền, ngày, upload ảnh

2.2. Backend — mô hình MVC

Backend Yotea theo mô hình MVC (Model – View – Controller), nhưng vì đây là API thuần nên không có View; thứ trả về cho client là JSON. Còn lại đủ ba lớp:

Thư mục Vai trò Ví von
routes/ Khai báo URL nào ứng với hàm nào, và request đó phải đi qua chốt kiểm tra nào Bảng chỉ dẫn ở cửa: "Đơn thêm sản phẩm → đưa cho bếp trưởng, phải có thẻ nhân viên"
controllers/ Chứa logic xử lý: đọc dữ liệu client gửi lên, gọi Model, trả kết quả Đầu bếp thật sự làm việc
models/ Mô tả cấu trúc dữ liệu và nói chuyện với MongoDB Công thức pha chế + kho nguyên liệu
middlewares/ Các chốt kiểm tra chạy trước controller Bảo vệ kiểm tra thẻ ra vào

Điểm quan trọng cần nhớ ngay từ bây giờ: ba thư mục routes, controllers, models có các file trùng tên nhau. Ví dụ với sản phẩm, bạn sẽ thấy bộ ba:

yotea-be/src/routes/product.js       ← URL nào
yotea-be/src/controllers/product.js  ← làm gì
yotea-be/src/models/product.js       ← dữ liệu trông ra sao

Cứ nắm quy tắc này, muốn sửa chức năng nào bạn biết ngay phải mở 3 file nào.

2.3. Trái tim của backend: app.js

yotea-be/src/app.js:22-42

const app = express();

// middleware
app.use(express.json());
app.use(cors());
app.use(morgan("tiny"));

app.use("/api", categoryRouter);
app.use("/api", productRouter);
app.use("/api", sliderRouter);
app.use("/api", storeRouter);
app.use("/api", cateNewsRouter);
app.use("/api", newsRouter);
app.use("/api", contactRouter);
app.use("/api", userRouter);
app.use("/api", authRouter);
app.use("/api", orderRouter);
app.use("/api", orderDetailRouter);
app.use("/api", cmtRouter);
app.use("/api", ratingRouter);
app.use("/api", favoritesRouter);

Đọc từng dòng:

Dòng Code Ý nghĩa
22 const app = express() Tạo ra ứng dụng web. Từ đây app chính là server.
25 app.use(express.json()) Bật khả năng đọc dữ liệu JSON client gửi lên. Không có dòng này, req.body sẽ luôn là undefined.
26 app.use(cors()) Cho phép trình duyệt ở cổng 3000 gọi sang cổng 8080. Không có dòng này trình duyệt sẽ chặn mọi request.
27 app.use(morgan("tiny")) In log mỗi request ra terminal, cực hữu ích khi debug.
29–42 app.use("/api", xxxRouter) Gắn 14 nhóm route vào server, tất cả đều có tiền tố /api.

Chính vì dòng 29–42 mà mọi địa chỉ API của dự án đều bắt đầu bằng /api: http://localhost:8080/api/products, http://localhost:8080/api/signin

Ngay dưới đó là phần kết nối database và bật server:

yotea-be/src/app.js:44-52

// connect db
mongoose
  .connect("mongodb://localhost:27017/yotea")
  .then(() => console.log("Connected to MongoDB"))
  .catch((error) => console.log(error));

// connect
const PORT = process.env.PORT || 8080;
app.listen(PORT, () => console.log(`App is running on port: ${PORT}`));

⚠️ Chỗ này dự án làm chưa chuẩn: địa chỉ database bị viết cứng (hardcode) ngay trong code. Chuẩn công nghiệp là đưa vào biến môi trường (file .env) để mỗi môi trường — máy bạn, máy đồng nghiệp, server thật — dùng một địa chỉ khác nhau mà không phải sửa code. Ta sẽ sửa đúng chỗ này ở Bài 34.

2.4. Trái tim của frontend: index.jsApp.js

yotea-fe/src/index.js:10-19

const root = ReactDOM.createRoot(document.getElementById("root"));
root.render(
  <React.StrictMode>
    <Provider store={store}>
      <PersistGate loading={null} persistor={persistor}>
        <App />
      </PersistGate>
    </Provider>
  </React.StrictMode>
);

Đọc từ ngoài vào trong như bóc củ hành:

Lớp Nhiệm vụ
createRoot(document.getElementById("root")) Tìm thẻ <div id="root"> trong public/index.html — toàn bộ web sẽ được vẽ vào đây
<React.StrictMode> Chế độ kiểm tra nghiêm ngặt khi lập trình (khiến một số hàm chạy 2 lần — đừng hoảng)
<Provider store={store}> Đưa kho dữ liệu Redux xuống cho mọi component bên trong dùng chung
<PersistGate> Chờ đọc xong dữ liệu đã lưu trong trình duyệt (giỏ hàng, phiên đăng nhập) rồi mới vẽ giao diện
<App /> Ứng dụng thật sự — chứa toàn bộ bảng đường dẫn

Còn App.jsbản đồ đường đi của cả website: mỗi địa chỉ trên thanh URL ứng với một component nào. Ví dụ một mẩu trong đó:

yotea-fe/src/App.js:64-84

const router = createBrowserRouter([
  {
    path: "",
    element: <WebsiteLayout />,
    children: [
      {
        path: "",
        element: <HomePage />,
      },
      {
        path: "gioi-thieu",
        element: <AboutPage />,
      },
      {
        path: "thuc-don",
        element: <ProductPage />,
      },

Nghĩa là: vào localhost:3000/thuc-don thì React sẽ vẽ WebsiteLayout (header + footer), và bên trong nó vẽ ProductPage. Cơ chế layout lồng nhau này sẽ được mổ kỹ ở Bài 15.


3. 🛠️ Tự tay làm — lần theo một chức năng qua 7 lớp

Mục tiêu phần này: cuối phần bạn sẽ tự chỉ được ngón tay vào 7 file và nói "dữ liệu đi từ đây, qua đây, tới đây". Đây là kỹ năng quan trọng nhất khi nhận bàn giao một dự án lạ.

Chức năng ta chọn: trang thực đơn hiển thị danh sách sản phẩm.

Bước 1 — Xác định URL người dùng truy cập

Người dùng vào http://localhost:3000/thuc-don.

Bước 2 — Tìm xem URL đó vẽ component nào

Mở yotea-fe/src/App.js, dùng Ctrl + F tìm chuỗi thuc-don. Bạn sẽ thấy:

{
  path: "thuc-don",
  element: <ProductPage />,
},

👉 Lớp 1: yotea-fe/src/App.js → chỉ tới ProductPage.

Bước 3 — Mở component đó

Mở yotea-fe/src/pages/user/ProductPage.js. Tìm các dòng có useEffect, dispatch hoặc tên hàm bắt đầu bằng get. Đây là nơi trang yêu cầu dữ liệu.

👉 Lớp 2: trang gọi lên Redux (hoặc gọi thẳng tầng api).

Bước 4 — Lần tới Redux

Mở yotea-fe/src/redux/productSlice.js. Chú ý đoạn này:

yotea-fe/src/redux/productSlice.js:9-19

export const getProducts = createAsyncThunk(
  "product/getProducts",
  async ({ start, limit }) => {
    const { data } = await getAll();
    const totalProduct = data.length;

    const { data: productsData } = await getAll(start, limit);

    return { totalProduct, productsData };
  }
);

Nó gọi hàm getAll — hàm này được import từ ../api/product.

👉 Lớp 3: Redux gọi xuống tầng api.

Bước 5 — Xem tầng api tạo ra request gì

Mở yotea-fe/src/api/product.js:

yotea-fe/src/api/product.js:9-18

export const getAll = (
  start = 0,
  limit = 0,
  sort = "createdAt",
  order = "desc"
) => {
  let url = `/${DB_NAME}/?_expand=categoryId&_sort=${sort}&_order=${order}`;
  if (limit) url += `&_start=${start}&_limit=${limit}`;
  return instance.get(url);
};

Kết hợp với yotea-fe/src/api/instance.js (đặt baseURLhttp://localhost:8080/api), request thật sự bay đi là:

GET http://localhost:8080/api/products/?_expand=categoryId&_sort=createdAt&_order=desc

👉 Lớp 4: trình duyệt bắn HTTP request sang backend. Đây chính là ranh giới giữa frontend và backend.

Bước 6 — Backend nhận request

Mở yotea-be/src/routes/product.js:

yotea-be/src/routes/product.js:8-13

router.post("/products/:userId", requireSignin, isAuth, isAdmin, create);
router.get("/products/:slug", read);
router.get("/products", list);
router.put("/products/:id/:userId", requireSignin, isAuth, isAdmin, update);
router.patch("/products/userUpdate/:id", clientUpdate);
router.delete("/products/:id/:userId", requireSignin, isAuth, isAdmin, remove);

GET /products khớp dòng thứ ba → chạy hàm list.

👉 Lớp 5: router chọn đúng controller.

Bước 7 — Controller truy vấn database

Mở yotea-be/src/controllers/product.js, tìm export const list. Cuối hàm là:

const products = await Product.find(filter)
  .select("-__v")
  .populate(populate)
  .skip(start)
  .limit(limit)
  .sort(sortOpt)
  .exec();
res.json(products);

Product chính là model, import từ ../models/product.

👉 Lớp 6: controller gọi model. 👉 Lớp 7: model (yotea-be/src/models/product.js) nói chuyện với MongoDB, lấy dữ liệu về, controller đóng gói thành JSON res.json(products) và trả ngược lại đúng con đường vừa đi.

Bước 8 — Vẽ lại sơ đồ bằng chính tay bạn

Lấy giấy bút (hoặc mở file text), viết lại chuỗi 7 lớp trên mà không nhìn tài liệu:

URL /thuc-don
  → App.js
  → ProductPage.js
  → productSlice.js
  → api/product.js  ═══ HTTP ═══▶  routes/product.js
  → controllers/product.js
  → models/product.js
  → MongoDB

Viết được trơn tru nghĩa là bạn đã nắm xương sống của dự án.


4. ✅ Kiểm chứng kết quả

Bạn hoàn thành bài này khi trả lời được không cần mở tài liệu:

Câu hỏi Đáp án đúng
Muốn sửa giao diện trang thực đơn thì vào thư mục nào? yotea-fe/src/pages/user/
Muốn thêm một API mới thì phải sửa ít nhất mấy file, những file nào? 3 file: routes/, controllers/, models/ (thêm 1 dòng app.use nếu là router mới)
Frontend gọi backend qua địa chỉ nào? http://localhost:8080/api/..., khai báo trong yotea-fe/src/api/instance.js
Vì sao mọi API đều có /api ở đầu? app.js mount router bằng app.use("/api", xxxRouter)
middlewares/checkAuth.js dùng để làm gì? Kiểm tra đã đăng nhập chưa và có phải admin không, chạy trước controller

Và mở được đủ 7 file trong phần "Tự tay làm" mà không phải tìm mò.


5. 🐞 Lỗi thường gặp

Vấn đề Nguyên nhân Cách xử lý
Mở yotea-be/src/app.js thấy import mà nhớ Node.js phải dùng require Dự án dùng Babel để dịch cú pháp ES6 sang cú pháp Node hiểu được (xem file .babelrc) Bình thường, cứ viết import theo dự án
Không tìm thấy thư mục views/ dù được nói là MVC Đây là API thuần, không render HTML, nên không có View Đúng như thiết kế, không thiếu gì cả
Tìm mãi không thấy file .env Dự án chưa dùng biến môi trường, mọi cấu hình đều hardcode Ghi nhớ để sửa ở Bài 34
Thấy node_modules nặng vài trăm MB và cũng nằm trong git Thư viện tải về, lẽ ra phải bỏ qua bằng .gitignore Không đụng vào thư mục này, không bao giờ sửa file trong đó
Thấy yotea-fe/src/api/product.js vừa có getAll vừa có productApi Dự án dùng song song 2 cách lấy dữ liệu: axios thủ công và RTK Query Sẽ được giải thích ở Bài 22

6. 📝 Bài tập

Bài 1. Dự án có 14 nhóm chức năng ở backend. Hãy mở thư mục yotea-be/src/routes/, liệt kê đủ 14 file và ghi cạnh mỗi file một câu mô tả nghiệp vụ mà nó phụ trách.

<details> <summary>💡 Xem gợi ý & lời giải</summary>
File route Nghiệp vụ
auth.js Đăng ký, đăng nhập, kiểm tra mật khẩu
users.js Quản lý tài khoản người dùng (admin CRUD)
category.js Danh mục sản phẩm (Trà sữa, Trà trái cây…)
product.js Sản phẩm — trung tâm của web bán hàng
slider.js Ảnh banner chạy ở đầu trang chủ
store.js Hệ thống cửa hàng / chi nhánh
cateNews.js Danh mục bài viết
news.js Bài viết / tin tức
contact.js Liên hệ khách gửi về
order.js Đơn hàng (thông tin người nhận, tổng tiền, trạng thái)
orderDetail.js Chi tiết từng dòng sản phẩm trong đơn
comment.js Bình luận dưới sản phẩm
rating.js Đánh giá sao
favoritesProduct.js Sản phẩm yêu thích

Điểm cộng nếu bạn nhận ra: trong yotea-be/src/models/14 model nhưng routes/ chỉ có 14 file cho 14 nhóm khác. Cụ thể models/voucher.js tồn tại nhưng không có routes/voucher.js lẫn controllers/voucher.js — nghĩa là chức năng mã giảm giá mới làm được phần dữ liệu, chưa có API. Đây chính là đề tài của Bài 35 — Đồ án cuối khoá.

</details>

Bài 2. Lặp lại bài tập "lần theo 7 lớp" nhưng với chức năng danh sách tin tức (URL /tin-tuc). Ghi ra đủ tên 7 file bạn phải mở.

<details> <summary>💡 Xem gợi ý & lời giải</summary>
URL /tin-tuc
  1. yotea-fe/src/App.js            → tìm path "tin-tuc" → <NewsPage />
  2. yotea-fe/src/pages/user/NewsPage.js
  3. yotea-fe/src/redux/newsSlice.js
  4. yotea-fe/src/api/news.js        → sinh URL GET /api/news?...
     ══════════════ HTTP ══════════════
  5. yotea-be/src/routes/news.js     → router.get("/news", list)
  6. yotea-be/src/controllers/news.js → export const list
  7. yotea-be/src/models/news.js     → model News → MongoDB

Bạn có nhận ra cấu trúc lặp lại y hệt với sản phẩm không? Đó không phải trùng hợp — đây là điểm mạnh của việc code theo quy ước: học một lần, áp dụng cho cả 14 chức năng.

</details>

Bài 3. (nâng cao) Mở yotea-be/src/routes/product.jsyotea-be/src/routes/news.js. Route nào bắt buộc đăng nhập, route nào ai cũng gọi được? Dựa vào đâu mà bạn biết?

<details> <summary>💡 Xem gợi ý & lời giải</summary>

Nhìn vào các tham số đứng giữa đường dẫn và hàm xử lý:

router.get("/products", list);                                              // ai cũng gọi được
router.post("/products/:userId", requireSignin, isAuth, isAdmin, create);   // phải đăng nhập + là admin

requireSignin, isAuth, isAdmin chính là middleware — các chốt kiểm tra chạy tuần tự trước create. Chốt nào không cho qua thì request dừng ngay tại đó, create không bao giờ được chạy.

Quy luật chung của dự án: đọc (GET) thì tự do, ghi (POST/PUT/DELETE) thì phải là admin. Riêng PATCH /products/userUpdate/:id (tăng lượt xem, lượt thích) là ngoại lệ có chủ ý — khách vãng lai cũng phải làm được. Chi tiết ở Bài 12.

</details>

📌 Tóm tắt

  • Yotea gồm 3 tiến trình chạy song song: React (:3000) → Express (:8080) → MongoDB (:27017).
  • Frontend không bao giờ nói chuyện thẳng với database; mọi thứ đi qua backend bằng HTTP + JSON.
  • Backend theo mô hình MVC không View: routes (URL nào) → controllers (làm gì) → models (dữ liệu ra sao), cộng thêm middlewares làm chốt kiểm tra.
  • Ba thư mục routes / controllers / modelsfile trùng tên nhau — nhớ quy tắc này là biết ngay phải mở file nào khi sửa chức năng.
  • Frontend chia thành pages (trang) · components (mảnh dùng lại) · api (gọi backend) · redux (dữ liệu dùng chung) · utils (tiện ích).
  • Mọi API đều có tiền tố /apiapp.js mount router bằng app.use("/api", xxxRouter).
  • Kỹ năng cần luyện: lần theo một chức năng qua đủ 7 lớp — dùng được cho mọi dự án sau này.

Từ khoá tra cứu thêm: MERN stack, client-server architecture, REST API, MVC pattern, separation of concerns

➡️ Bài tiếp theo: 02 — Cài đặt môi trường và chạy dự án lần đầu — cài Node, MongoDB và nhìn thấy trang chủ Yotea hiện ra trên máy bạn.


All Rights Reserved

Viblo
Let's register a Viblo Account to get more interesting posts.