Insights

Backend

14 Ways to Level Up API CRUD from Naive to Production-Ready

Upgrade CRUD from basic to professional: Validation, Pagination, Cache, Transaction, Rate Limit, Versioning and more.

19/02/2026 · 11 min read · Lê Huy Thái

Main point
  • 14 techniques divided into 3 groups: Read Request (GET), Write Request (POST/PUT/DELETE), Applies to all
  • From naive raw query to DTO → Pagination → Filter/Search/Sort → Cache
  • Recording side: Validation → Soft Delete → Transaction → Bulk Operations
  • Common platform: Standardization, Auth, Error Handling, Rate Limit, Versioning, Health Check
  • Most frameworks/ORMs come pre-packaged — just need to know how to enable them in the right places

Bored or insecure with the basic CRUD API? Sharing with you 14 ways to Level Up the CRUD API, adding ideas to apply to products or projects for internships is also good.

CRUD (Create, Read, Update, Delete) is the backbone of every application. We, the developers, will have to code it and re-code it hundreds, thousands of times in our careers. Almost 99% of business logic is reduced to CRUD.

So for those of you who are new (new to work, new to school, or just switching from FE to BE), have you ever wondered: Why does my CRUD sometimes feel a bit fragile, a bit sketchy, a bit lacking?

Have you ever wondered what else people will write and do in big projects, real and professional products, to make that basic CRUD become better, more secure and more realistic?

In this article, I will share some concepts, patterns, and solutions so you can level up your request, from Level 0: Innocence to being able to work.

I. Read Request (GET)

These are Requests that do not change data (e.g. GET /products).

Level 0: Innocence — Raw Query Starting point

Query the database (DB), return the same. SELECT * FROM product then return to FE.

The problem is that it returns sensitive fields (password, token) or is redundant, very dangerous for security.


Method 1: Clean up data Clean

There are 2 popular solutions:

  • Solution 1: Only SELECT the necessary fields instead of SELECT *.
  • Solution 2 (Convert DTO): Add a conversion step from DB data to Response DTO to return only necessary data to FE.
-- Thay vì:
SELECT * FROM users;

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

Method 2: Pagination Pagination

Problem: If a Read Request returns 1 billion records without paging, it will "kill" the server and "kill" the client.

Solution: ALWAYS PAGE for APIs that may return large numbers of records.

TypeFor exampleWhen to use
Page/Limit?page=2&limit=20Most popular, used in most cases
Offset/Limit?offset=40&limit=20When precise control of starting position is required
Cursor-based?cursor=abc123&limit=20Unlimited feed (Facebook, Twitter), realtime data

Method 3: Increase user experience Filter / Search / Sort

Problem: User has to continue the page continuously to find the red product.

Solution: Add basic but extremely important functions: Filter, Search, Sort.

GET /products?filter[color]=gold&sort[price]=asc&search=iphone

Method 4: Speed ​​up with Cache Caching

Problem: There are APIs that change very little (e.g. Product Review). But every time a user logs in, the system queries the DB once.

Solution: Cache data and Invalidate Cache when data changes.

  1. 01
    Request arrives → Check Cache

    Each request first looks up the cache key (usually Redis) corresponding to the filter.

  2. 02
    Cache Hit → Return immediately

    Data exists in cache → returns immediately, extremely fast, without touching the DB.

  3. 03
    Cache Miss → Query DB → Save Cache

    Not in cache → query DB → save result in cache with TTL → return.

  4. 04
    When Write/Update → Invalidate Cache

    When a record is added/edited/deleted → delete or reset the related cache key.

// 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;
}
II. Request Write (POST, PUT, PATCH, DELETE)

These are Requests that change data.

Level 0: Innocence — Absolute trust in FE Starting point

What does FE give → Throw to DB. No testing, no validation. Extremely dangerous.


Method 5: Validation Filter dirty data

Must validate in all 3 places and no place can be trusted:

PlacePurposeFor example
FrontendGood UX, quick response to usersAn error appears immediately when entering the wrong email format
BackendSecurity, business logicCheck email uniqueness, inventory quantity
DatabaseLast line of defenseConstraint 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);

Method 6: Soft delete Soft Delete

Problem: Using DELETE removes data from the DB → There is no way to restore.

Solution: Use Soft Delete. Instead of physically deleting, add a deleted_at field. When deleting, only Update this field. When querying, remember to remove soft-deleted records.

-- 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;

Method 7: Transaction Transaction

Problem: Occurs in complex, inherited logic. For example: Purchase = Create Order → Subtract Product Quantity → Create Payment. If the middle step fails, the previous steps were created (business error).

Solution: Use Transaction. Make sure all DB logic wrapped in Transaction SAME SUCCEED OR FAIL. If there is an error, the previous steps will be Rollback, as if they never existed.

Nguyên tắc vàng
All or Nothing

Transactions guarantee atomicity: either all steps in the chain succeed, or none are recorded. This is the foundation of data consistency in any real system.

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

Method 8: Bulk Operations Bulk Operations

Problem: User wants to delete 1000 products at the same time. If you call DELETE /products/{id} 1000 times, it will be very slow, waste resources, and can cause timeouts.

Solution: Support Bulk Operations — process multiple records in a single request.

Bulk Operations API Design
// POST /products/bulk-delete
// Body: { ids: [1, 2, 3, ...] }
// POST /products/bulk-update
// Body: { updates: [{ id: 1, price: 100 }, { id: 2, price: 200 }] }
III. Applies to all Requests

The rules apply to ALL requests.

Method 9: Standardization and consistency Standardization

Three golden rules:

1. Doing it according to RESTful standards:

MethodEndpointDescribe
GET/productsGet the list
GET/products/:idGet details
POST/productsCreate new
PUT/products/:idFull update
PATCH/products/:idPartial update
DELETE/products/:idErase

2. Return correct HTTP Status Code:

CodeMeaningWhen to use
200OKGET, PUT, PATCH successful
201CreatedPOST created successfully
204No ContentDELETE successful
400Bad RequestValidation error
401UnauthorizedNot logged in yet
403ForbiddenNo rights
404Not FoundResources not found
429Too Many RequestsExceed Rate Limit
500Internal Server ErrorServer error

3. Consistent Response Format:

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

Method 10: Identification and authorization Auth

Two concepts need to be clearly distinguished:

  • Authentication: Tells who you are — often using JWT Token, Session, OAuth.
  • Authorization: Shows what you can do — using Role-based (RBAC) or Permission-based.
Ưu điểm
  • Only logged in users (Authentication) can place orders
  • Only Admin (Authorization) can manage products
  • Users can usually only edit/delete their own data
Nhược điểm
  • It's easy to confuse Authentication and Authorization
  • Lack of authorization leads to user A being able to view/edit user B's data
  • No Auth → entire public API, anyone can call

Method 11: Error Handling and Logging Error Handling

Problem: Unhandled error will return 500 Internal Server Error automatically, dev has no log to debug.

Wrap the try-catch and return reasonable errors to 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' });
}

Error logging: When encountering an unexpected error (Error 500), the log must be saved to a 3rd party file or service (Sentry, Datadog) so the team can know and handle it.


Method 12: Rate limit Rate Limit

Problem: API is heavy (Long queries, Upload large files), API is expensive (Calls to AI service in turns), API is susceptible to spam (Comments, Reviews).

Solution: Apply Rate Limit — limit the number of Requests per time period/user.

For example: Product Review API only gets 1 Request/minute. When limit is exceeded → Returns 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);

Method 13: Create version Versioning

Problem: Mobile applications that have been uploaded to the Store cannot arbitrarily change the structure of the returned API. If you change the response format, the old app will crash.

Solution: Create a new API version when there are major changes.

/api/v1/products    → Response cũ (vẫn hoạt động)
/api/v2/products    → Response mới (thêm field, đổi format)


Method 14: Health check and monitoring Health Check

Problem: Not sure if the API is live, if the Database can be connected, if Redis is working. When something goes wrong, we don't know what the cause is.

Solution: Create a Health Check endpoint to check the status of the system.

// 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);
});
Summary

So let's take a look at 14 things that can be applied to "cook up" basic CRUD:

GroupLevels
Request Read (GET)Raw Query → DTO → Pagination → Filter/Sort/Search → Cache
Request Write (POST/PUT/DELETE)FE News → Validation → Soft Delete → Transaction → Bulk Ops
All RequestsStandardization → Auth → Error Handling → Rate Limit → Versioning → Health Check

Instead of CRUD code like "put it back up, why return it?", apply these things to make your project look more professional and ready for reality.

Is there anything missing?

If you have any more practical, easy-to-apply patterns that I haven't mentioned — please contact TensorLab. Let's build better CRUD together!