TensorLab logoTensorLab
HomeCapabilitiesPartnershipProjectsBlogTeamContact

Ready to start your project?

Get in touch to discuss your ideas.

TensorLab logoTensorLab

Tech partner for product partnership and outsourcing. Focused on AI, Web3, and pragmatic digital transformation.

Accepting new projects
lethai2597@gmail.com0961 741 678

Services

  • Capabilities
  • Engagement models
  • Delivery process

Partnership

  • Product partnership
  • Outsource
  • Contact

© 2026 TensorLab. All rights reserved.

Facebook·Email
Back to Blog

14 cách Level Up API CRUD từ Ngây Thơ đến Production-Ready

February 19, 202613 phút đọc

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

  1. Thứ tự từ dễ đến khó — Nếu anh em thấy những phần đầu quá dễ hoặc hiển nhiên, cứ skip đến phần sau nhé.
  2. Dành cho người mới/muốn nâng cấp — Anh em có trên 1-2 năm kinh nghiệm có thể thấy bình thường, nhưng đây là kiến thức sống còn để code không bị "mong manh".
  3. Concept chung — Những thứ mình giới thiệu là pattern và concept. Anh em áp dụng vào ngôn ngữ/framework của mình thì research thêm nhé, rất có thể framework đã đóng gói sẵn rồi.
  4. Sử dụng thuật ngữ "Request" — Mình dùng từ Request thay vì API để nó là từ chung, áp dụng được cho cả API (BE/FE) lẫn các hệ thống MVC.

Ok, bắt đầu luôn!


I. Request dạng Đọc (GET)#

Đây là những Request không làm thay đổi dữ liệu (ví dụ: GET /products).

Level 0: Ngây Thơ (Raw Query)#

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.


Level 1: Dọn dẹp dữ liệu (Sạch sẽ)#

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:

  • Giải pháp 1: Chỉ SELECT những field cần thiết thay vì SELECT *.
  • Giải pháp 2 (Convert DTO): Thêm một bước convert từ dữ liệu DB sang Response DTO để chỉ trả những dữ liệu cần thiết cho FE.
sql
-- Thay vì:
SELECT * FROM users;

-- Hãy:
SELECT id, name, email, avatar FROM users;

Level 2: Phân trang (Pagination)#

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ểuVí dụKhi nào dùng
Page/Limit?page=2&limit=20Phổ biến nhất, dùng cho hầu hết trường hợp
Offset/Limit?offset=40&limit=20Khi cần kiểm soát chính xác vị trí bắt đầu
Cursor-based?cursor=abc123&limit=20Feed vô hạn (Facebook, Twitter), dữ liệu realtime

Level 3: Tăng trải nghiệm người dùng (UX)#

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.


Level 4: Tăng tốc với Cache (Caching)#

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:

  1. Request đến -> Kiểm tra Cache
  2. Cache Hit -> Trả về ngay (cực nhanh)
  3. Cache Miss -> Query DB -> Lưu vào Cache -> Trả về
  4. Khi Write/Update -> Lưu DB -> Invalidate Cache Key liên quan
javascript
// 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ũ.


II. Request dạng Ghi (POST, PUT, PATCH, DELETE)#

Đây là những Request làm thay đổi dữ liệu.

Level 0: Ngây Thơ (Tin tưởng FE tuyệt đối)#

FE đưa gì -> Quăng vào DB. Không kiểm tra, không validate. Nguy hiểm cực kỳ.


Level 1: Validation (Lọc dữ liệu bẩn)#

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ơiMục đíchVí dụ
FrontendUX tốt, phản hồi nhanh cho userHiện lỗi ngay khi nhập sai format email
BackendBảo mật, logic nghiệp vụKiểm tra email unique, số lượng tồn kho
DatabaseTuyến phòng thủ cuối cùngConstraint NOT NULL, UNIQUE, CHECK
javascript
// 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);

Level 2: Xóa mềm (Soft Delete)#

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.

sql
-- 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.


Level 3: Giao tác (Transaction)#

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.

javascript
// 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

Level 4: Thao tác hàng loạt (Bulk Operations)#

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.

javascript
// 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

  • Bọc trong Transaction để đảm bảo tính nhất quán (tất cả thành công hoặc tất cả thất bại).
  • Giới hạn số lượng bản ghi trong một bulk request (ví dụ: tối đa 100 items) để tránh quá tải.

III. Request nói chung (Áp dụng cho tất cả)#

Những nguyên tắc áp dụng được cho TẤT CẢ mọi Request.

Level 1: Chuẩn hóa và nhất quán (Standardization)#

Ba nguyên tắc vàng:

1. Làm đúng chuẩn RESTful:

MethodEndpointMô tả
GET/productsLấy danh sách
GET/products/:idLấy chi tiết
POST/productsTạo mới
PUT/products/:idCập nhật toàn bộ
PATCH/products/:idCập nhật một phần
DELETE/products/:idXóa

2. Trả đúng HTTP Status Code:

CodeÝ nghĩaKhi nào dùng
200OKGET, PUT, PATCH thành công
201CreatedPOST tạo mới thành công
204No ContentDELETE thành công
400Bad RequestValidation lỗi
401UnauthorizedChưa đăng nhập
403ForbiddenKhông có quyền
404Not FoundKhông tìm thấy resource
429Too Many RequestsVượt Rate Limit
500Internal Server ErrorLỗi server

3. Nhất quán Response Format:

json
{
  "success": true,
  "data": { "id": 1, "name": "iPhone 16" },
  "message": "Product created successfully"
}

Level 2: Định danh và phân quyền (Auth)#

Hai khái niệm cần phân biệt rõ:

  • Authentication (Xác thực): Cho biết bạn là ai — thường dùng JWT Token, Session, OAuth.
  • Authorization (Phân quyền): Cho biết bạn có thể làm được gì — dùng Role-based (RBAC) hoặc Permission-based.

Áp dụng thực tế:

  • Chỉ user đã đăng nhập (Authentication) mới được đặt hàng.
  • Chỉ Admin (Authorization) mới được quản lý sản phẩm.
  • User thường chỉ được sửa/xóa dữ liệu của chính mình.

Level 3: Xử lý lỗi và Logging (Error Handling)#

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:

javascript
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.


Level 4: Giới hạn tốc độ (Rate Limit)#

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.

javascript
// 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);

Level 5: Tạo phiên bản (Versioning)#

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?

  • Thay đổi cấu trúc response (breaking change)
  • Xóa/đổi tên field
  • Thay đổi logic xử lý lớn

Không cần versioning cho: thêm field mới, fix bug, cải thiện performance.


Level 6: Kiểm tra sức khỏe và giám sát (Health Check)#

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.

javascript
// 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.


Tổng kết#

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ómLevels
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ả RequestStandardization -> 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é.