Chán hoặc không yên tâm với API CRUD cơ bản? Chia sẻ với anh em 14 cách Level Up API CRUD lên, thêm ý tưởng áp dụng cho sản phẩm hoặc project để đi thực tập cũng ngon nhé.
CRUD (Create, Read, Update, Delete) chính là xương sống của mọi ứng dụng. Chúng ta, những dev, sẽ phải code nó đi code nó lại cả trăm, cả ngàn lần trong sự nghiệp. Gần như 99% logic nghiệp vụ đều quy về CRUD hết.
Vậy với các bạn mới (mới đi làm, mới học, hoặc mới rẽ từ FE sang BE), có bao giờ anh em tự hỏi: Tại sao CRUD của mình nhiều lúc cảm giác nó hơi mong manh, hơi sơ sài, hơi thiếu thiếu không?
Có bao giờ anh em thắc mắc là ở những dự án lớn, những sản phẩm thực tế và chuyên nghiệp, người ta sẽ viết thêm, làm thêm những cái gì để cái CRUD cơ bản đấy trở nên xịn hơn, bảo mật hơn và thực chiến hơn không?
Trong bài viết này, mình sẽ chia sẻ một số concept, pattern, và giải pháp để anh em có thể level up cái request của mình lên, từ Level 0: Ngây Thơ đến có thể đi làm được.
Lưu ý trước khi bắt đầu
Ok, bắt đầu luôn!
Đây là những Request không làm thay đổi dữ liệu (ví dụ: GET /products).
Query database (DB) thế nào, trả về y nguyên thế đó. SELECT * FROM product xong return cho FE.
Vấn đề là nó trả ra cả những field nhạy cảm (password, token) hoặc thừa thãi, rất nguy hiểm về bảo mật.
Nguyên tắc sống còn
KHÔNG BAO GIỜ trả trực tiếp raw query từ database.
Có 2 giải pháp phổ biến:
SELECT những field cần thiết thay vì SELECT *.-- Thay vì:
SELECT * FROM users;
-- Hãy:
SELECT id, name, email, avatar FROM users;
Vấn đề: Nếu một Request đọc trả về 1 tỷ bản ghi mà không phân trang, nó sẽ "đạp chết" server và "đạp chết" client.
Giải pháp: LUÔN LUÔN PHÂN TRANG cho các API có thể trả về số lượng lớn bản ghi.
Các kiểu thường dùng:
| Kiểu | Ví dụ | Khi nào dùng |
|---|---|---|
| Page/Limit | ?page=2&limit=20 | Phổ biến nhất, dùng cho hầu hết trường hợp |
| Offset/Limit | ?offset=40&limit=20 | Khi cần kiểm soát chính xác vị trí bắt đầu |
| Cursor-based | ?cursor=abc123&limit=20 | Feed vô hạn (Facebook, Twitter), dữ liệu realtime |
Vấn đề: User phải next trang liên tục để tìm sản phẩm màu đỏ.
Giải pháp: Thêm các chức năng cơ bản nhưng cực kỳ quan trọng: Lọc (Filter), Tìm kiếm (Search), Sắp xếp (Sort).
GET /products?filter[color]=gold&sort[price]=asc&search=iphone
Mẹo thực tế
Hãy thiết kế query params nhất quán ngay từ đầu. Nhiều framework hỗ trợ sẵn convention này, ví dụ Laravel có Eloquent Query Builder, NestJS có class-transformer.
Vấn đề: Có những API cực ít thay đổi (ví dụ: Review sản phẩm). Nhưng mỗi khi user vào, hệ thống lại query DB một lần.
Giải pháp: Cache dữ liệu và Invalidate Cache khi dữ liệu thay đổi.
Flow có Cache:
// Ví dụ với Redis
async function getProducts(filters) {
const cacheKey = `products:${JSON.stringify(filters)}`;
// Kiểm tra cache
const cached = await redis.get(cacheKey);
if (cached) return JSON.parse(cached);
// Cache miss -> query DB
const products = await db.products.findMany(filters);
// Lưu cache (TTL 5 phút)
await redis.setex(cacheKey, 300, JSON.stringify(products));
return products;
}
Lưu ý về Cache Invalidation
Cache Invalidation là một trong những bài toán khó nhất trong Computer Science. Hãy cẩn thận với việc invalidate đúng key khi dữ liệu thay đổi, tránh tình trạng user thấy dữ liệu cũ.
Đây là những Request làm thay đổi dữ liệu.
FE đưa gì -> Quăng vào DB. Không kiểm tra, không validate. Nguy hiểm cực kỳ.
Nguyên tắc sống còn
LUÔN PHẢI VALIDATE DỮ LIỆU trước khi xử lý hoặc lưu vào DB. Phải lọc dữ liệu bẩn từ người dùng.
Phải validate ở đủ 3 nơi và không nơi nào được tin tưởng nơi nào:
| Nơi | Mục đích | Ví dụ |
|---|---|---|
| Frontend | UX tốt, phản hồi nhanh cho user | Hiện lỗi ngay khi nhập sai format email |
| Backend | Bảo mật, logic nghiệp vụ | Kiểm tra email unique, số lượng tồn kho |
| Database | Tuyến phòng thủ cuối cùng | Constraint NOT NULL, UNIQUE, CHECK |
// Ví dụ validation với Zod (Node.js)
const createProductSchema = z.object({
name: z.string().min(1).max(255),
price: z.number().positive(),
quantity: z.number().int().min(0),
email: z.string().email(),
});
// Validate trước khi xử lý
const validated = createProductSchema.parse(req.body);
Vấn đề: Dùng DELETE bay màu dữ liệu khỏi DB -> Không có cách nào khôi phục.
Giải pháp: Sử dụng Soft Delete. Thay vì xóa vật lý, thêm một field deleted_at. Khi xóa, chỉ Update field này. Khi query, nhớ loại bỏ các bản ghi đã bị xóa mềm.
-- Thay vì:
DELETE FROM products WHERE id = 1;
-- Soft Delete:
UPDATE products SET deleted_at = NOW() WHERE id = 1;
-- Khi query, luôn thêm điều kiện:
SELECT * FROM products WHERE deleted_at IS NULL;
Framework hỗ trợ sẵn
Hầu hết ORM đều hỗ trợ Soft Delete built-in: Eloquent (Laravel), Sequelize, Prisma, TypeORM... Chỉ cần bật lên là xong, không cần code tay.
Vấn đề: Xảy ra trong các logic phức tạp, có tính kế thừa. Ví dụ: Mua hàng = Tạo Order -> Trừ Số Lượng Product -> Tạo Payment. Nếu bước giữa lỗi, các bước trước đó đã được tạo (lỗi nghiệp vụ).
Giải pháp: Sử dụng Transaction. Đảm bảo tất cả mọi logic DB bọc trong Transaction CÙNG THÀNH CÔNG HOẶC THẤT BẠI. Nếu lỗi, các bước trước đó sẽ được Rollback, như chưa từng tồn tại.
// Ví dụ Transaction với Prisma
await prisma.$transaction(async (tx) => {
// Bước 1: Tạo Order
const order = await tx.order.create({ data: orderData });
// Bước 2: Trừ số lượng (nếu lỗi ở đây, bước 1 cũng rollback)
await tx.product.update({
where: { id: productId },
data: { quantity: { decrement: orderQuantity } },
});
// Bước 3: Tạo Payment
await tx.payment.create({ data: { orderId: order.id, ...paymentData } });
});
// Nếu bất kỳ bước nào lỗi -> TẤT CẢ đều rollback
Vấn đề: User muốn xóa 1000 sản phẩm cùng lúc. Nếu gọi 1000 lần DELETE /products/{'{id}'}, sẽ rất chậm, tốn tài nguyên, và có thể gây timeout.
Giải pháp: Hỗ trợ Bulk Operations — xử lý nhiều bản ghi trong một request duy nhất.
// POST /products/bulk-delete
// Body: { ids: [1, 2, 3, ...] }
// POST /products/bulk-update
// Body: { updates: [{ id: 1, price: 100 }, { id: 2, price: 200 }] }
Lưu ý quan trọng
Những nguyên tắc áp dụng được cho TẤT CẢ mọi Request.
Ba nguyên tắc vàng:
1. Làm đúng chuẩn RESTful:
| Method | Endpoint | Mô tả |
|---|---|---|
GET | /products | Lấy danh sách |
GET | /products/:id | Lấy chi tiết |
POST | /products | Tạo mới |
PUT | /products/:id | Cập nhật toàn bộ |
PATCH | /products/:id | Cập nhật một phần |
DELETE | /products/:id | Xóa |
2. Trả đúng HTTP Status Code:
| Code | Ý nghĩa | Khi nào dùng |
|---|---|---|
200 | OK | GET, PUT, PATCH thành công |
201 | Created | POST tạo mới thành công |
204 | No Content | DELETE thành công |
400 | Bad Request | Validation lỗi |
401 | Unauthorized | Chưa đăng nhập |
403 | Forbidden | Không có quyền |
404 | Not Found | Không tìm thấy resource |
429 | Too Many Requests | Vượt Rate Limit |
500 | Internal Server Error | Lỗi server |
3. Nhất quán Response Format:
{
"success": true,
"data": { "id": 1, "name": "iPhone 16" },
"message": "Product created successfully"
}
Hai khái niệm cần phân biệt rõ:
Áp dụng thực tế:
Vấn đề: Lỗi không được xử lý (unhandled error) sẽ trả về 500 Internal Server Error một cách tự động, dev không có log để debug.
Giải pháp:
1. Bọc try-catch và trả lỗi hợp lý cho FE:
try {
const product = await productService.create(data);
return res.status(201).json({ success: true, data: product });
} catch (error) {
if (error instanceof ValidationError) {
return res.status(400).json({ success: false, message: error.message });
}
// Lỗi không lường trước
logger.error('Create product failed', { error, data });
return res.status(500).json({ success: false, message: 'Internal Server Error' });
}
2. Logging lỗi: Khi gặp lỗi không lường trước (Lỗi 500), phải lưu log vào file hoặc dịch vụ thứ 3 (Sentry, Datadog) để team biết và xử lý.
Mẹo
Đừng bao giờ trả error stack trace cho FE trong production. Chỉ log ở server, trả message thân thiện cho user.
Vấn đề: API nặng (Query lâu, Upload File lớn), API tốn tiền (Gọi sang dịch vụ AI theo lượt), API dễ bị spam (Comment, Review).
Giải pháp: Áp dụng Rate Limit — giới hạn số lượng Request trên một khoảng thời gian/user.
Ví dụ: API Review sản phẩm chỉ được 1 Request/phút. Khi vượt quá giới hạn -> Trả về 429 Too Many Requests.
// Ví dụ Rate Limit config
const rateLimiter = rateLimit({
windowMs: 60 * 1000, // 1 phút
max: 10, // Tối đa 10 requests
message: { success: false, message: 'Too many requests, please try again later.' },
});
app.use('/api/', rateLimiter);
Vấn đề: Ứng dụng Mobile đã lên Store không thể tùy tiện thay đổi cấu trúc API trả ra. Nếu đổi response format, app cũ sẽ crash.
Giải pháp: Tạo phiên bản API mới khi có thay đổi lớn.
/api/v1/products → Response cũ (vẫn hoạt động)
/api/v2/products → Response mới (thêm field, đổi format)
Khi nào cần Versioning?
Không cần versioning cho: thêm field mới, fix bug, cải thiện performance.
Vấn đề: Không biết API có đang sống không, Database có kết nối được không, Redis có hoạt động không. Khi có sự cố, không biết nguyên nhân ở đâu.
Giải pháp: Tạo Health Check endpoint để kiểm tra trạng thái của hệ thống.
// GET /health
app.get('/health', async (req, res) => {
const health = {
status: 'ok',
timestamp: new Date().toISOString(),
checks: {
database: await checkDB(), // Có kết nối được không?
redis: await checkRedis(), // Có hoạt động không?
diskSpace: checkDisk(), // Còn đủ dung lượng không?
},
};
const isHealthy = Object.values(health.checks).every(c => c === 'ok');
res.status(isHealthy ? 200 : 503).json(health);
});
Ứng dụng thực tế
Monitoring tools (Prometheus, Grafana, UptimeRobot) sẽ gọi endpoint này định kỳ để phát hiện sự cố sớm. Khi status khác "ok", hệ thống sẽ tự động alert cho team. Health Check phải nhanh và không tốn tài nguyên vì nó được gọi rất thường xuyên.
Vậy là chúng ta đã cùng điểm qua 14 thứ có thể áp dụng để "xào nấu" lại cái CRUD cơ bản:
| Nhóm | Levels |
|---|---|
| Request Đọc (GET) | Raw Query -> DTO -> Pagination -> Filter/Sort/Search -> Cache |
| Request Ghi (POST/PUT/DELETE) | Tin FE -> Validation -> Soft Delete -> Transaction -> Bulk Ops |
| Tất cả Request | Standardization -> Auth -> Error Handling -> Rate Limit -> Versioning -> Health Check |
Thay vì code CRUD kiểu "đưa sao lưu vậy, có sao trả ra vậy", hãy áp dụng những thứ này để dự án của anh em trông chuyên nghiệp hơn và sẵn sàng cho thực tế hơn nhé.
Tạm thời thế đã. Còn thiếu gì hay, thực tiễn, dễ áp dụng thì anh em comment thêm nhé.