Series Total TypeScript Thực chiến #10: Sự kết hợp hoàn hảo với Zod – Tường đồng vách sắt (Trạm cuối)
1. Ảo tưởng sức mạnh của Type tĩnh
Hãy tưởng tượng bạn đang viết một API để xử lý giao dịch mua vé cho hệ thống AFC Metro. Bạn định nghĩa một Interface rất chặt chẽ:
interface TicketPurchasePayload {
stationId: string;
amount: number;
isMonthlyPass: boolean;
}
function processPurchase(payload: TicketPurchasePayload) {
// Logic trừ tiền, in vé...
}
Mọi thứ trông rất an toàn lúc gõ code. Nhưng khi lên Production, một Hacker (hoặc một Client code ẩu) gửi một Request với body dạng JSON như sau:
{
"stationId": "S01",
"amount": "15000",
"isMonthlyPass": "true"
}
Lưu ý: amount và isMonthlyPass đang là dạng chuỗi (string), không phải số (number) hay boolean. Vì hệ thống Type đã biến mất, Node.js sẽ thản nhiên đưa cục JSON này vào hàm processPurchase, dẫn đến việc lưu sai dữ liệu vào Database hoặc làm sập hệ thống tính toán.
2. Cứu tinh Zod: Người gác cổng Runtime
Thay vì tự viết hàng chục dòng lệnh if (typeof payload.amount !== 'number') rất tốn thời gian, cộng đồng Node.js hiện tại tôn thờ thư viện Zod.
Zod cho phép bạn định nghĩa các Schema (bản thiết kế cấu trúc) hoạt động thực sự ở Runtime.
import { z } from 'zod';
// Định nghĩa Zod Schema (Nguồn chân lý duy nhất)
const ticketPurchaseSchema = z.object({
stationId: z.string().min(3).max(10), // Bắt buộc là string, độ dài từ 3-10
amount: z.number().positive(), // Bắt buộc là số dương
isMonthlyPass: z.boolean(), // Bắt buộc là boolean
});
Khi có dữ liệu bay vào API, bạn chỉ cần ném nó qua hàm parse() của Zod. Nếu dữ liệu sai cấu trúc, Zod sẽ lập tức quăng ra một Exception báo lỗi cực kỳ chi tiết, chặn đứng việc dữ liệu bẩn lọt vào sâu bên trong.
3. Tuyệt chiêu z.infer: Nối liền hai thế giới
Bây giờ bạn sẽ tự hỏi: "Nếu dùng Zod Schema, thế thì tôi lại phải viết một cái Interface TypeScript có cấu trúc y hệt để dùng cho các hàm xử lý bên dưới à? Thế thì code lại bị lặp (duplicate) rồi!"
Đây chính là lúc tinh hoa của Matt Pocock lên tiếng. Nhờ việc vận dụng tối đa các kỹ thuật nội suy (infer) và Mapped Types mà chúng ta đã học từ Bài 1 đến Bài 5, Zod cung cấp một hàm tiện ích giúp bắt Schema đẻ ra Type.
import { z } from 'zod';
const ticketPurchaseSchema = z.object({
stationId: z.string(),
amount: z.number(),
isMonthlyPass: z.boolean(),
});
// 🔥 TUYỆT CHIÊU KẾT HỢP: Rút trích Type tĩnh từ Zod Schema
type TicketPurchasePayload = z.infer<typeof ticketPurchaseSchema>;
/*
Kết quả: TicketPurchasePayload đã trở thành Type tĩnh hoàn hảo:
{
stationId: string;
amount: number;
isMonthlyPass: boolean;
}
*/
Từ giờ trở đi, khi Database Schema thay đổi, bạn chỉ cần sửa duy nhất đối tượng Zod Schema. Hệ thống Type tĩnh của TypeScript sẽ tự động biến hình chạy theo ngay lập tức. Tính toàn vẹn của mã nguồn (Single Source of Truth) được bảo vệ tuyệt đối.
4. Thực chiến: Đúc Middleware kiểm duyệt vạn năng
Để kết thúc series, chúng ta hãy mang kiến thức Generics ở Bài 7 kết hợp với Zod để viết ra một Middleware xác thực dữ liệu API dùng chung cho mọi Controller trong Express.js.
import { Request, Response, NextFunction } from 'express';
import { z, AnyZodObject } from 'zod';
// Hàm nhận vào một Zod Schema (T) và trả về một Middleware
const validateRequest = (schema: AnyZodObject) => {
return async (req: Request, res: Response, next: NextFunction) => {
try {
// Xác thực Body của Request với Schema
req.body = await schema.parseAsync(req.body);
next();
} catch (error) {
// Bắt lỗi Validation và trả về mã 400
if (error instanceof z.ZodError) {
return res.status(400).json({
status: 'error',
message: 'Dữ liệu đầu vào không hợp lệ',
errors: error.issues,
});
}
next(error);
}
};
};
// ------------------------------------
// ỨNG DỤNG TẠI ROUTER:
// ------------------------------------
import express from 'express';
const app = express();
app.use(express.json());
// Gắn cái gác cổng bằng Zod vào Route xử lý mua vé
app.post(
'/api/v1/tickets/purchase',
validateRequest(ticketPurchaseSchema), // Middleware chặn dữ liệu rác
(req, res) => {
// 💡 Tại đây, req.body ĐÃ ĐƯỢC ĐẢM BẢO 100% khớp với cấu trúc Zod
const { stationId, amount } = req.body as z.infer<typeof ticketPurchaseSchema>;
// Xử lý nghiệp vụ an toàn tuyệt đối...
res.json({ message: `Đã xử lý thanh toán ${amount} cho ga ${stationId}` });
}
);
Lời kết Series Total TypeScript
Hành trình 10 bài học đã chính thức khép lại. Đi từ những thao tác "móc ruột" Array bằng Indexed Access, nhào nặn chuỗi bằng Template Literals, rẽ nhánh bằng Conditional Types, cho đến việc thiết kế những hàm Generics vạn năng và chốt hạ bằng lớp tường thành Runtime của Zod.
Bạn đã nắm trong tay toàn bộ kho vũ khí của một Senior TypeScript Engineer. Những hệ thống lớn, những bộ microservices hay các module lõi phức tạp giờ đây sẽ không thể làm khó được bạn. Code của bạn không chỉ chạy đúng, mà còn trở thành những bản thiết kế kiến trúc thanh lịch, sạch sẽ và bền vững trước mọi thay đổi của nghiệp vụ.
Chúc bạn và các anh em kỹ sư áp dụng thành công những kỹ thuật này để xây dựng những hệ thống Backend mạnh mẽ, xử lý hàng triệu giao dịch mỗi ngày mà không lọt một bug nào!
All rights reserved