Xây dựng CRUD API sản phẩm
Bài trước: Lesson 4: Giới thiệu MongoDB và Mongoose
Bài tiếp theo: Lesson 6: Middleware validate dữ liệu đầu vào
Mục tiêu
- Thực hành xây dựng API CRUD sản phẩm đầy đủ với MongoDB và Mongoose.
- Hiểu cách tổ chức code với models, controllers, và routers.
- Làm quen với cách xử lý lỗi và phản hồi trạng thái HTTP.
Yêu cầu
Tạo cấu trúc thư mục
- Tạo các thư mục
models,controllers, vàrouterstrong thư mụcsrc. - Tạo file
product.model.jstrong thư mụcmodelsđể định nghĩa schema và model cho sản phẩm. - Tạo file
product.controller.jstrong thư mụccontrollersđể xử lý logic CRUD. - Tạo file
product.router.jstrong thư mụcroutersđể định nghĩa các endpoint API.
- Tạo các thư mục
Định nghĩa schema và model cho sản phẩm
- Các trường cần có:
name(String, bắt buộc, tối đa 200 ký tự).slug(String, duy nhất, viết thường).description(String, bắt buộc).price(Number, bắt buộc, không âm).salePrice(Number, không âm).images(Array of Strings).stock(Number, bắt buộc, không âm, mặc định là 0).status(Enum:draft,published,archived, mặc định làdraft).featured(Boolean, mặc định làfalse).ratings(Number, từ 0 đến 5, làm tròn đến 1 chữ số thập phân).
- Sử dụng Mongoose để tạo model từ schema.
- Các trường cần có:
Tách logic xử lý CRUD vào controller
- Tạo các hàm xử lý trong controller:
- Lấy danh sách sản phẩm (
GET /api/products). - Lấy chi tiết sản phẩm theo
id(GET /api/products/:id). - Thêm sản phẩm mới (
POST /api/products). - Cập nhật sản phẩm theo
id(PUT /api/products/:id). - Xóa sản phẩm theo
id(DELETE /api/products/:id).
- Lấy danh sách sản phẩm (
- Tạo các hàm xử lý trong controller:
Tạo router cho sản phẩm
- Định nghĩa các endpoint CRUD trong file router.
- Sử dụng các hàm từ controller để xử lý logic.
Tích hợp router vào ứng dụng chính
- Import router từ
src/routers/product.router.jsvàosrc/routers/index.js. - Gắn router vào đường dẫn
/products.
- Import router từ
Kiểm tra API
- Sử dụng Postman hoặc công cụ tương tự để kiểm tra các endpoint CRUD.
Hướng dẫn
Thiết lập cấu trúc thư mục
src/
├── models/
│ └── product.model.js # Định nghĩa schema và model cho sản phẩm
├── controllers/
│ └── product.controller.js # Xử lý logic CRUD cho sản phẩm
├── routers/
│ ├── index.js # Tệp chính định nghĩa các router
│ └── product.router.js # Định nghĩa các endpoint API cho sản phẩm
└── app.js # Tệp chính khởi chạy ứng dụngĐịnh nghĩa Schema và Model
import mongoose from "mongoose";
const productSchema = new mongoose.Schema(
{
name: {
type: String,
required: [true, "Tên sản phẩm là bắt buộc"],
trim: true,
maxlength: [200, "Tên sản phẩm không được vượt quá 200 ký tự"],
},
slug: {
type: String,
unique: true,
lowercase: true,
},
description: {
type: String,
required: [true, "Mô tả sản phẩm là bắt buộc"],
},
price: {
type: Number,
required: [true, "Giá sản phẩm là bắt buộc"],
min: [0, "Giá sản phẩm không được âm"],
},
salePrice: {
type: Number,
min: [0, "Giá khuyến mãi không được âm"],
},
images: [String],
stock: {
type: Number,
required: [true, "Số lượng tồn kho là bắt buộc"],
min: [0, "Số lượng tồn kho không được âm"],
default: 0,
},
status: {
type: String,
enum: ["draft", "published", "archived"],
default: "draft",
},
featured: {
type: Boolean,
default: false,
},
ratings: {
type: Number,
default: 0,
min: [0, "Đánh giá thấp nhất là 0"],
max: [5, "Đánh giá cao nhất là 5"],
set: (val) => Math.round(val * 10) / 10, // Làm tròn đến 1 chữ số thập phân
},
},
{ timestamps: true, versionKey: false }
);
const Product = mongoose.model("Product", productSchema);
export default Product;3. Các bước cần làm trước khi viết Controller
Trước khi bắt tay vào viết code cho controller, chúng ta cần xác định rõ các bước cần thực hiện để đảm bảo logic được xây dựng đúng và đầy đủ. Dưới đây là các bước cụ thể:
3.1. Lấy danh sách sản phẩm (GET /api/products)
- Kết nối cơ sở dữ liệu:
- Sử dụng model
Productđể truy vấn danh sách sản phẩm.
- Sử dụng model
- Xử lý kết quả:
- Nếu có sản phẩm, trả về danh sách sản phẩm.
- Nếu xảy ra lỗi, trả về lỗi server.
3.2. Lấy chi tiết sản phẩm (GET /api/products/:id)
- Nhận
idtừ URL:- Lấy
idtừreq.params.
- Lấy
- Tìm sản phẩm trong cơ sở dữ liệu:
- Sử dụng
Product.findByIdđể tìm sản phẩm theoid.
- Sử dụng
- Xử lý kết quả:
- Nếu tìm thấy sản phẩm, trả về thông tin sản phẩm.
- Nếu không tìm thấy, trả về lỗi
404 Not Found. - Nếu xảy ra lỗi, trả về lỗi server.
3.3. Thêm sản phẩm mới (POST /api/products)
- Nhận dữ liệu từ client:
- Các trường cần nhận:
name,slug,description,price,salePrice,images,stock,status,featured,ratings.
- Các trường cần nhận:
- Kiểm tra dữ liệu đầu vào:
- Đảm bảo các trường bắt buộc đều có giá trị.
- Kiểm tra các ràng buộc như
price >= 0,stock >= 0,ratingstừ 0 đến 5.
- Lưu sản phẩm vào cơ sở dữ liệu:
- Sử dụng model
Productđể lưu thông tin sản phẩm.
- Sử dụng model
- Trả về phản hồi:
- Nếu thành công, trả về thông tin sản phẩm vừa thêm.
- Nếu có lỗi, trả về thông báo lỗi chi tiết.
3.4. Cập nhật sản phẩm (PUT /api/products/:id)
- Nhận
idtừ URL và dữ liệu từ client:- Lấy
idtừreq.paramsvà dữ liệu cập nhật từreq.body.
- Lấy
- Tìm và cập nhật sản phẩm trong cơ sở dữ liệu:
- Sử dụng
Product.findByIdAndUpdateđể cập nhật sản phẩm theoid. - Đảm bảo chạy các validator khi cập nhật.
- Sử dụng
- Xử lý kết quả:
- Nếu cập nhật thành công, trả về thông tin sản phẩm đã cập nhật.
- Nếu không tìm thấy sản phẩm, trả về lỗi
404 Not Found. - Nếu xảy ra lỗi, trả về thông báo lỗi chi tiết.
3.5. Xóa sản phẩm (DELETE /api/products/:id)
- Nhận
idtừ URL:- Lấy
idtừreq.params.
- Lấy
- Tìm và xóa sản phẩm trong cơ sở dữ liệu:
- Sử dụng
Product.findByIdAndDeleteđể xóa sản phẩm theoid.
- Sử dụng
- Xử lý kết quả:
- Nếu xóa thành công, trả về thông báo thành công.
- Nếu không tìm thấy sản phẩm, trả về lỗi
404 Not Found. - Nếu xảy ra lỗi, trả về lỗi server.
4. Tách Controller để quản lý logic
Sau khi xác định rõ các bước cần làm, chúng ta sẽ viết code cho các chức năng trong file controller.
import Product from "../models/product.model";
// Lấy danh sách sản phẩm với pagination, filtering và sorting
export const getProducts = async (req, res) => {
try {
const {
page = 1,
limit = 10,
status,
featured,
minPrice,
maxPrice,
sort = "-createdAt", // Mặc định sắp xếp theo ngày tạo mới nhất
} = req.query;
// Xây dựng query filter
const filter = {};
if (status) filter.status = status;
if (featured !== undefined) filter.featured = featured === "true";
if (minPrice || maxPrice) {
filter.price = {};
if (minPrice) filter.price.$gte = Number(minPrice);
if (maxPrice) filter.price.$lte = Number(maxPrice);
}
// Tính toán pagination
const skip = (Number(page) - 1) * Number(limit);
// Thực hiện query với pagination và sorting
const products = await Product.find(filter).sort(sort).skip(skip).limit(Number(limit));
// Đếm tổng số sản phẩm phù hợp với filter
const total = await Product.countDocuments(filter);
// Tính toán thông tin pagination
const totalPages = Math.ceil(total / Number(limit));
res.json({
success: true,
data: products,
pagination: {
currentPage: Number(page),
totalPages,
totalItems: total,
itemsPerPage: Number(limit),
hasNextPage: Number(page) < totalPages,
hasPreviousPage: Number(page) > 1,
},
});
} catch (err) {
return res.status(500).json({ error: "Lỗi server", message: err.message });
}
};
// Lấy chi tiết sản phẩm
export const getProductById = async (req, res) => {
try {
const product = await Product.findById(req.params.id);
if (!product) return res.status(404).json({ error: "Không tìm thấy sản phẩm" });
res.json(product);
} catch (err) {
return res.status(500).json({ error: "Lỗi server", message: err.message });
}
};
// Thêm sản phẩm mới
export const createProduct = async (req, res) => {
try {
const newProduct = await Product.create(req.body);
return res.status(201).json(newProduct);
} catch (err) {
return res.status(400).json({ error: "Lỗi khi thêm sản phẩm", message: err.message });
}
};
// Cập nhật sản phẩm
export const updateProduct = async (req, res) => {
try {
const product = await Product.findByIdAndUpdate(req.params.id, req.body, {
new: true,
runValidators: true,
});
if (!product) return res.status(404).json({ error: "Không tìm thấy sản phẩm" });
res.json(product);
} catch (err) {
return res.status(400).json({ error: "Lỗi khi cập nhật sản phẩm", message: err.message });
}
};
// Xóa sản phẩm
export const deleteProduct = async (req, res) => {
try {
const product = await Product.findByIdAndDelete(req.params.id);
if (!product) return res.status(404).json({ error: "Không tìm thấy sản phẩm" });
res.json({ success: true });
} catch (err) {
return res.status(500).json({ error: "Lỗi server", message: err.message });
}
};Sử dụng Controller trong Router
import { Router } from "express";
import {
getProducts,
getProductById,
createProduct,
updateProduct,
deleteProduct,
} from "../controllers/product.controller";
const routeProduct = Router();
// Lấy danh sách sản phẩm
routeProduct.get("/", getProducts);
// Lấy chi tiết sản phẩm
routeProduct.get("/:id", getProductById);
// Thêm sản phẩm mới
routeProduct.post("/", createProduct);
// Cập nhật sản phẩm
routeProduct.put("/:id", updateProduct);
// Xóa sản phẩm
routeProduct.delete("/:id", deleteProduct);
export default routeProduct;Import router vào file src/routers/index.js
Để sử dụng các router đã tạo, bạn cần import chúng vào file routers/index.js và cấu hình như sau:
import { Router } from "express";
import routePost from "./post.router";
import routeProduct from "./product.router";
const router = Router();
// Sử dụng router cho bài viết
router.use("/posts", routePost);
// Sử dụng router cho sản phẩm
router.use("/products", routeProduct);
export default router;4. Test API với Postman và Dữ liệu Fake
4.1. Dữ liệu Fake
Dưới đây là một số dữ liệu mẫu để kiểm tra API:
Thêm sản phẩm mới (POST /api/products)
- Body (JSON):
{
"name": "Laptop Dell XPS 13",
"slug": "laptop-dell-xps-13",
"description": "Laptop cao cấp với thiết kế mỏng nhẹ.",
"price": 35000,
"salePrice": 32000,
"images": ["https://example.com/image1.jpg", "https://example.com/image2.jpg"],
"stock": 10,
"status": "published",
"featured": true,
"ratings": 4.5
}- Kết quả:
{
"_id": "64f1a2b3c4d5e6f7g8h9i0j1",
"name": "Laptop Dell XPS 13",
"slug": "laptop-dell-xps-13",
"description": "Laptop cao cấp với thiết kế mỏng nhẹ.",
"price": 35000,
"salePrice": 32000,
"images": ["https://example.com/image1.jpg", "https://example.com/image2.jpg"],
"stock": 10,
"status": "published",
"featured": true,
"ratings": 4.5,
"createdAt": "2023-09-01T12:00:00.000Z",
"updatedAt": "2023-09-01T12:00:00.000Z"
}Lấy danh sách sản phẩm (GET /api/products)
- Kết quả:
[
{
"_id": "64f1a2b3c4d5e6f7g8h9i0j1",
"name": "Laptop Dell XPS 13",
"slug": "laptop-dell-xps-13",
"price": 35000,
"stock": 10,
"status": "published",
"ratings": 4.5
},
{
"_id": "64f1a2b3c4d5e6f7g8h9i0j2",
"name": "iPhone 14 Pro Max",
"slug": "iphone-14-pro-max",
"price": 45000,
"stock": 5,
"status": "published",
"ratings": 4.8
}
]Lấy chi tiết sản phẩm (GET /api/products/:id)
URL:
http://localhost:3000/api/products/64f1a2b3c4d5e6f7g8h9i0j1Kết quả:
{
"_id": "64f1a2b3c4d5e6f7g8h9i0j1",
"name": "Laptop Dell XPS 13",
"slug": "laptop-dell-xps-13",
"description": "Laptop cao cấp với thiết kế mỏng nhẹ.",
"price": 35000,
"salePrice": 32000,
"images": ["https://example.com/image1.jpg", "https://example.com/image2.jpg"],
"stock": 10,
"status": "published",
"featured": true,
"ratings": 4.5,
"createdAt": "2023-09-01T12:00:00.000Z",
"updatedAt": "2023-09-01T12:00:00.000Z"
}Cập nhật sản phẩm (PUT /api/products/:id)
URL:
http://localhost:3000/api/products/64f1a2b3c4d5e6f7g8h9i0j1Body (JSON):
{
"price": 34000,
"stock": 15
}- Kết quả:
{
"_id": "64f1a2b3c4d5e6f7g8h9i0j1",
"name": "Laptop Dell XPS 13",
"slug": "laptop-dell-xps-13",
"description": "Laptop cao cấp với thiết kế mỏng nhẹ.",
"price": 34000,
"salePrice": 32000,
"images": ["https://example.com/image1.jpg", "https://example.com/image2.jpg"],
"stock": 15,
"status": "published",
"featured": true,
"ratings": 4.5,
"createdAt": "2023-09-01T12:00:00.000Z",
"updatedAt": "2023-09-01T12:30:00.000Z"
}Xóa sản phẩm (DELETE /api/products/:id)
URL:
http://localhost:3000/api/products/64f1a2b3c4d5e6f7g8h9i0j1Kết quả:
{
"success": true
}5. Pagination, Filtering và Sorting
5.1. Pagination
Pagination giúp chia nhỏ kết quả thành các trang, đặc biệt hữu ích khi có nhiều sản phẩm. API GET /api/products đã được cập nhật để hỗ trợ pagination:
Query Parameters:
page: Số trang (mặc định: 1)limit: Số sản phẩm mỗi trang (mặc định: 10)
Ví dụ:
GET /api/products?page=1&limit=5Response:
{
"success": true,
"data": [...],
"pagination": {
"currentPage": 1,
"totalPages": 5,
"totalItems": 50,
"itemsPerPage": 5,
"hasNextPage": true,
"hasPreviousPage": false
}
}5.2. Filtering
Lọc sản phẩm theo các tiêu chí:
status: Lọc theo trạng thái (draft,published,archived)featured: Lọc sản phẩm nổi bật (true/false)minPrice: Giá tối thiểumaxPrice: Giá tối đa
Ví dụ:
GET /api/products?status=published&featured=true&minPrice=10000&maxPrice=500005.3. Sorting
Sắp xếp sản phẩm theo các trường:
sort: Trường sắp xếp (thêm-để sắp xếp giảm dần)price: Sắp xếp theo giá-price: Sắp xếp theo giá giảm dầncreatedAt: Sắp xếp theo ngày tạo (cũ nhất trước)-createdAt: Sắp xếp theo ngày tạo (mới nhất trước)
Ví dụ:
GET /api/products?sort=-price&page=1&limit=105.4. Kết hợp các tính năng
Bạn có thể kết hợp pagination, filtering và sorting:
GET /api/products?status=published&minPrice=20000&maxPrice=100000&sort=-createdAt&page=2&limit=206. Tóm tắt
- CRUD Operations: Đầy đủ các thao tác Create, Read, Update, Delete
- Pagination: Chia nhỏ kết quả thành các trang
- Filtering: Lọc sản phẩm theo nhiều tiêu chí
- Sorting: Sắp xếp kết quả theo các trường khác nhau
- Dữ liệu Fake: Sử dụng các mẫu JSON để kiểm tra các endpoint CRUD
- Test Postman: Kiểm tra các endpoint
/api/productsvới các phương thức và query parameters
Nếu có thắc mắc, đừng ngại hỏi thầy hoặc các bạn nhé!
Chúc các em học tốt! 🚀 — Thầy Đạt 🧡