# Bài 05 — Kết nối MongoDB & API đầu tiên
⬅️ Bài trước | Mục lục | Bài tiếp theo ➡️
🎯 Mục tiêu
- Viết file
model/db.jsđể kết nối MongoDB - Hiểu Router của Express — cách chia nhỏ route ra nhiều file
- Viết API hoàn chỉnh đầu tiên:
GET /categories - Test API bằng trình duyệt và Postman
💻 1. File backend/model/db.js
Tạo thư mục model trong backend, rồi tạo file db.js:
const { MongoClient } = require('mongodb');
const url = "mongodb://localhost:27017";
const dbName = 'lith_perfume';
async function connectDb() {
const client = new MongoClient(url);
await client.connect();
console.log('Kết nối thành công đến server');
return client.db(dbName);
}
module.exports = connectDb;
🔍 Giải thích từng dòng
const { MongoClient } = require('mongodb');
Lấy class MongoClient từ thư viện mongodb (destructuring — bài 02, mục 4).
const url = "mongodb://localhost:27017";
Connection string — địa chỉ MongoDB. Cấu trúc:
mongodb://[user:password@]host:port[/database][?options]
↑ ↑ ↑ ↑
giao thức đăng nhập máy cổng
Chạy local không cần user/password. Bài 22 (deploy) sẽ dùng chuỗi dạng
mongodb+srv://user:pass@cluster.mongodb.net/... của MongoDB Atlas.
const dbName = 'lith_perfume';
Tên database đã tạo ở bài 03. Phải khớp chính xác, phân biệt hoa thường.
async function connectDb() {
const client = new MongoClient(url);
await client.connect();
return client.db(dbName);
}
new MongoClient(url)— tạo đối tượng client (chưa kết nối)await client.connect()— thực sự mở kết nối, mất vài chục msclient.db(dbName)— trả về đối tượng database để thao tác
Vì có await nên hàm phải là async, và mọi nơi gọi nó cũng phải await.
⚠️ 2. Vấn đề của cách viết này (quan trọng)
Cách viết trên là cách của source gốc — nó chạy được nhưng có nhược điểm lớn: mỗi request tạo một kết nối mới.
router.get("/", async (req, res) => {
const db = await connectDb(); // ← mở kết nối MỚI
// ...
}); // ← và không bao giờ đóng
Bạn sẽ thấy console in ra Kết nối thành công đến server liên tục —
mỗi lần load trang là vài lần. Với 100 người dùng đồng thời, MongoDB sẽ hết kết nối.
✅ Cách viết tốt hơn: dùng lại kết nối (connection reuse)
// backend/model/db.js — PHIÊN BẢN CẢI TIẾN
const { MongoClient } = require('mongodb');
const url = process.env.MONGODB_URI || "mongodb://localhost:27017";
const dbName = process.env.DB_NAME || 'lith_perfume';
// Biến lưu kết nối đã tạo, để dùng lại
let cachedDb = null;
const client = new MongoClient(url);
async function connectDb() {
// Nếu đã kết nối rồi thì trả về luôn, không mở lại
if (cachedDb) return cachedDb;
await client.connect();
console.log('✅ Đã kết nối MongoDB:', dbName);
cachedDb = client.db(dbName);
return cachedDb;
}
module.exports = connectDb;
Bạn dùng phiên bản nào cũng được — cả hai đều chạy đúng cho bài học. Tài liệu khuyến nghị dùng bản cải tiến vì:
- Console sạch hơn (chỉ in 1 lần)
- Nhanh hơn (không phải bắt tay lại mỗi request)
- Chuẩn bị sẵn cho
process.envkhi deploy (bài 22)
Từ bài này trở đi, tài liệu dùng bản cải tiến.
💡
let cachedDb = nullnằm ở cấp module nên chỉ chạy một lần khi file đượcrequirelần đầu. Node.js cache module — các lầnrequiresau đều nhận cùng một đối tượng.
📚 3. Express Router — chia route ra nhiều file
Nếu viết tất cả route vào app.js, file đó sẽ dài hàng nghìn dòng. Router giải quyết việc này.
Router là "mini app"
var express = require("express");
var router = express.Router(); // tạo router con
router.get("/", handler); // định nghĩa route
module.exports = router; // xuất ra để app.js dùng
Đường dẫn được ghép lại
// Trong app.js
app.use('/categories', categoriesRouter);
// Trong routes/categories.js
router.get("/", ...) → URL thật là GET /categories
router.get("/hot", ...) → URL thật là GET /categories/hot
router.get("/id/:id", ...) → URL thật là GET /categories/id/123
Router không biết tiền tố của nó. Đây là điểm hay: bạn có thể đổi tiền tố
trong app.js mà không cần sửa file router.
💻 4. API đầu tiên: GET /categories
Tạo file backend/routes/categories.js:
var express = require("express");
var router = express.Router();
// Import model
const connectDb = require("../model/db");
const { ObjectId } = require("mongodb");
// Lấy tất cả danh mục
router.get("/", async (req, res, next) => {
const db = await connectDb();
const categoriesCollection = db.collection("categories");
const categories = await categoriesCollection.find().toArray();
if (categories) {
res.status(200).json(categories);
} else {
res.status(404).json({ message: "Không tìm thấy" });
}
});
module.exports = router;
🔍 Bóc tách từng dòng
router.get("/", async (req, res, next) => {
Đăng ký: "khi có request GET tới đường dẫn / (của router này), chạy hàm sau".
Hàm là async vì bên trong có await.
const db = await connectDb();
Lấy đối tượng database. Phải await vì connectDb là async.
const categoriesCollection = db.collection("categories");
Lấy tham chiếu tới collection. Tên phải khớp chính xác với tên trong MongoDB
(categories, không phải Categories hay category).
const categories = await categoriesCollection.find().toArray();
.find()không tham số = lấy tất cả, trả về một cursor (con trỏ).toArray()đọc hết cursor thành mảng JavaScript
res.status(200).json(categories);
.status(200)đặt mã trạng thái.json(...)chuyển object/mảng thành chuỗi JSON, gắn headerContent-Type: application/json, và gửi đi
Có thể nối chuỗi (chaining) vì mỗi hàm đều return res.
⚠️ 5. Ba vấn đề của đoạn code trên
Vấn đề 1 — if (categories) luôn đúng
.toArray() luôn trả về mảng, kể cả rỗng []. Mà [] là truthy trong JavaScript:
if ([]) console.log("chạy vào đây"); // ← có in ra!
→ Nhánh else (404) không bao giờ chạy. Với danh sách thì đây là hành vi
đúng về mặt REST: danh sách rỗng vẫn là 200 [], không phải 404.
Nhưng ý định của người viết rõ ràng không phải vậy.
Vấn đề 2 — Không có try/catch
Nếu MongoDB tắt, await connectDb() sẽ ném lỗi. Trong hàm async không có
try/catch, lỗi biến thành unhandled promise rejection — Express 4 không bắt được.
Kết quả: request treo mãi, trình duyệt quay vòng, và bạn không biết lý do.
Vấn đề 3 — ObjectId được import nhưng không dùng
Ở file này ObjectId không cần thiết. Không gây lỗi, nhưng là code thừa.
✅ Phiên bản cải tiến
// backend/routes/categories.js — PHIÊN BẢN CẢI TIẾN
var express = require("express");
var router = express.Router();
const connectDb = require("../model/db");
const { ObjectId } = require("mongodb");
// GET /categories — lấy tất cả danh mục
router.get("/", async (req, res, next) => {
try {
const db = await connectDb();
const categories = await db.collection("categories").find().toArray();
res.status(200).json(categories);
} catch (error) {
next(error); // đẩy lỗi cho middleware xử lý lỗi trong app.js
}
});
// GET /categories/:id — lấy 1 danh mục (bổ sung, hữu ích cho bài 20)
router.get("/:id", async (req, res, next) => {
try {
if (!ObjectId.isValid(req.params.id)) {
return res.status(400).json({ message: "ID không hợp lệ" });
}
const db = await connectDb();
const category = await db.collection("categories")
.findOne({ _id: new ObjectId(req.params.id) });
if (!category) {
return res.status(404).json({ message: "Không tìm thấy danh mục" });
}
res.status(200).json(category);
} catch (error) {
next(error);
}
});
module.exports = router;
Ba cải tiến quan trọng:
try/catch+next(error)— lỗi được xử lý đàng hoàng, không treo requestObjectId.isValid()— kiểm tra trước khi tạoObjectId, tránh crash khi client gửi/categories/abcif (!category)— dùng!để kiểm tranull, đúng logic hơnif (category)
💡 Mẹo: dùng
return res.status(...)(córeturn) để dừng hàm ngay. Nếu quênreturn, code chạy tiếp xuống dưới và bạn sẽ gặp lỗiCannot set headers after they are sent to the client.
💻 6. Gắn router vào app.js
Bỏ comment các dòng liên quan tới categories trong backend/app.js:
var categoriesRouter = require('./routes/categories');
// ...
app.use('/categories', categoriesRouter);
Xóa luôn route /ping tạm ở bài 04 nếu muốn.
✅ 7. Kiểm thử
Cách 1 — Trình duyệt
Chạy npm run dev rồi mở http://localhost:5000/categories
Bạn phải thấy JSON:
[
{ "_id": "66758da7cb0dded448a58ba7", "name": "Valentino", "image": "h3.png" },
{ "_id": "66758e26cb0dded448a58ba8", "name": "Chloé", "image": "h5.png" },
...
]
💡 Cài extension JSON Viewer cho Chrome để JSON hiển thị đẹp và có thể gập/mở.
Thử luôn route mới: http://localhost:5000/categories/669a978e6e282b058a8e3f2c
→ phải ra { "_id": "...", "name": "Gucci", "image": "h8.png" }
Thử ID sai: http://localhost:5000/categories/abc
→ phải ra { "message": "ID không hợp lệ" } với status 400 (không phải trang lỗi 500)
Cách 2 — Postman / Thunder Client
- Method:
GET - URL:
http://localhost:5000/categories - Send
Kiểm tra ở panel kết quả:
- Status:
200 OK - Body: mảng 10 phần tử
- Headers: có
Content-Type: application/jsonvàAccess-Control-Allow-Origin: *
Header CORS đó chứng minh app.use(cors()) đang hoạt động.
Cách 3 — curl (dòng lệnh)
curl http://localhost:5000/categories
🆘 Xử lý sự cố
<details> <summary>Trả về mảng rỗng []</summary>Không có lỗi, chỉ là không tìm thấy dữ liệu. Kiểm tra theo thứ tự:
- Sai tên database? Trong
db.js,dbNamephải là'lith_perfume'. Kiểm tra trong Compass tên chính xác là gì. - Sai tên collection?
db.collection("categories")— kiểm tra chính tả, phân biệt hoa thường, số nhiều. - Chưa import dữ liệu? Trong Compass chạy
db.categories.countDocuments(), phải ra 10. Nếu ra 0 → quay lại bài 03 mục 5.
MongoDB chưa chạy. Đây là lý do phổ biến nhất.
Get-Service MongoDB # phải là Running
Start-Service MongoDB # nếu đang Stopped
Nếu dùng bản db.js cải tiến có try/catch trong route, bạn sẽ nhận được lỗi 500
kèm thông báo rõ ràng thay vì bị treo.
Node đang phân giải localhost thành IPv6 (::1) nhưng MongoDB chỉ nghe IPv4.
Sửa trong db.js:
const url = "mongodb://127.0.0.1:27017"; // dùng IP thay vì localhost
</details>
<details>
<summary>Cannot GET /categories</summary>
Router chưa được gắn vào app.js. Kiểm tra:
- Dòng
var categoriesRouter = require('./routes/categories');đã bỏ comment chưa - Dòng
app.use('/categories', categoriesRouter);đã bỏ comment chưa - File
routes/categories.jscómodule.exports = router;ở cuối chưa
📚 8. Tóm tắt luồng hoạt động
Trình duyệt gọi: GET http://localhost:5000/categories
│
▼
bin/www → server đang lắng nghe port 5000
│
▼
app.js → chạy qua chuỗi middleware:
cors() → morgan → express.json() → ...
│
▼
app.use('/categories', categoriesRouter)
URL bắt đầu bằng /categories → chuyển cho router
│
▼
routes/categories.js → router.get("/") khớp
│
▼
connectDb() → model/db.js → MongoClient kết nối
│
▼
db.collection("categories").find().toArray()
│
▼
MongoDB trả về 10 documents
│
▼
res.status(200).json(categories)
│
▼
Trình duyệt nhận JSON
📝 Bài tập
-
Thêm route
GET /categories/counttrả về{ total: 10 }. ⚠️ Chú ý thứ tự: route này phải đặt trướcGET /:id, nếu không Express sẽ hiểucountlà mộtid. Hãy thử đặt sau để tự chứng kiến lỗi. -
Thêm route
POST /categoriesđể tạo danh mục mới:{ "name": "Dior", "image": "h11.png" }Nhớ validate: thiếu
namethì trả về 400. -
Thêm query param sắp xếp:
GET /categories?sort=nametrả về danh sách đã sắp theo tên A→Z. Gợi ý:req.query.sortvà.sort({ name: 1 }). -
Tắt MongoDB (
Stop-Service MongoDB) rồi gọiGET /categories. So sánh hành vi giữa bản cótry/catchvà bản không có.
All rights reserved