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
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.
- Order from easy to difficult — If you find the first parts too easy or obvious, just skip to the next part.
- For newbies/wanting to upgrade — Those with more than 1–2 years of experience may find it normal, but this is vital knowledge to keep the code from being "fragile".
- General concept — The things I introduce are patterns and concepts. If you want to apply it to your language/framework, do more research, it's likely that the framework is already pre-packaged.
- Using the term "Request" — I use the word Request instead of API so that it is a common word, applicable to both API (BE/FE) and MVC systems.
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
NEVER return raw queries directly from the database.
There are 2 popular solutions:
- Solution 1: Only
SELECTthe necessary fields instead ofSELECT *. - 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.
| Type | For example | When to use |
|---|---|---|
| Page/Limit | ?page=2&limit=20 | Most popular, used in most cases |
| Offset/Limit | ?offset=40&limit=20 | When precise control of starting position is required |
| Cursor-based | ?cursor=abc123&limit=20 | Unlimited 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
Design consistent query params from the beginning. Many frameworks already support this convention, for example Laravel has Eloquent Query Builder, NestJS has class-transformer.
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.
- 01Request arrives → Check Cache
Each request first looks up the cache key (usually Redis) corresponding to the filter.
- 02Cache Hit → Return immediately
Data exists in cache → returns immediately, extremely fast, without touching the DB.
- 03Cache Miss → Query DB → Save Cache
Not in cache → query DB → save result in cache with TTL → return.
- 04When 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;
}
Cache Invalidation is one of the most difficult problems in Computer Science. Be careful about invalidating the correct key when data changes, to avoid users seeing old data.
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
ALWAYS VALIDATE DATA before processing or saving to DB. Must filter dirty data from users.
Must validate in all 3 places and no place can be trusted:
| Place | Purpose | For example |
|---|---|---|
| Frontend | Good UX, quick response to users | An error appears immediately when entering the wrong email format |
| Backend | Security, business logic | Check email uniqueness, inventory quantity |
| Database | Last line of defense | 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);
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;
Most ORMs support Soft Delete built-in: Eloquent (Laravel), Sequelize, Prisma, TypeORM... Just turn it on and you're done, no need to code manually.
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.
// 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.
// POST /products/bulk-delete
// Body: { ids: [1, 2, 3, ...] }
// POST /products/bulk-update
// Body: { updates: [{ id: 1, price: 100 }, { id: 2, price: 200 }] }
- Wrap in Transaction to ensure consistency (all successes or all failures).
- Limit the number of records in a bulk request (e.g. maximum 100 items) to avoid overload.
The rules apply to ALL requests.
Method 9: Standardization and consistency Standardization
Three golden rules:
1. Doing it according to RESTful standards:
| Method | Endpoint | Describe |
|---|---|---|
GET | /products | Get the list |
GET | /products/:id | Get details |
POST | /products | Create new |
PUT | /products/:id | Full update |
PATCH | /products/:id | Partial update |
DELETE | /products/:id | Erase |
2. Return correct HTTP Status Code:
| Code | Meaning | When to use |
|---|---|---|
200 | OK | GET, PUT, PATCH successful |
201 | Created | POST created successfully |
204 | No Content | DELETE successful |
400 | Bad Request | Validation error |
401 | Unauthorized | Not logged in yet |
403 | Forbidden | No rights |
404 | Not Found | Resources not found |
429 | Too Many Requests | Exceed Rate Limit |
500 | Internal Server Error | Server 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.
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.
Never return an error stack trace to FE in production. Just log in to the server, return a friendly message to the user.
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);
});
Monitoring tools (Prometheus, Grafana, UptimeRobot) will periodically call this endpoint to detect problems early. When the status is different from "ok", the system will automatically alert the team. Health Check should be fast and resource-free because it is called so frequently.
So let's take a look at 14 things that can be applied to "cook up" basic CRUD:
| Group | Levels |
|---|---|
| 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 Requests | Standardization → 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.