Chắc hẳn khi làm website, bạn đã từng đau đầu với việc dữ liệu "rác" được gửi từ client lên server: email sai định dạng, password quá ngắn, không đúng format bạn mong muốn, tên trống trơn,.... Những vấn đề tưởng chừng nhỏ nhặt này nhưng nếu không xử lý, chúng có thể gây ra siêu nhiều lỗi khó chịu, mất thời gian debug, hoặc thậm chí gây nguy cơ về bảo mật.
Trong TypeScript và Next.js, bạn có thể dễ dàng tạo API routes phục vụ phía frontend. Tuy nhiên, để đảm bảo dữ liệu gửi lên luôn đúng format, bạn cần một giải pháp xác thực dữ liệu (validation). Và đó chính là lý do bạn cần đến Zod. Vậy thì Zod là gì? Hãy cùng mình đi vọc nó qua bài này nha.
1. Zod là gì?
Zod là một thư viện JavaScript/TypeScript kiểm tra kiểu dữ liệu (type validation), giúp bạn xây dựng các schema mô tả format dữ liệu theo định dạng bạn mong muốn, sau đó dùng schema này để kiểm tra (parse) dữ liệu thực tế. Nếu dữ liệu không khớp với schema, Zod sẽ cho bạn biết ngay cái gì sai.
Bên cạnh đó, Zod giúp bạn tránh các bug do dữ liệu sai kiểu, đảm bảo tính nhất quán kiểu dữ liệu giữa client và server, code của bạn sẽ trông gọn gàng, dễ bảo trì.
Hiểu đơn giản như thế này:
- Bạn định nghĩa "Cấu trúc dữ liệu" bạn mong muốn (VD: email phải là chuỗi hợp lệ, password >= 6 ký tự).
- Khi dữ liệu từ form hoặc API gửi lên, bạn cho nó đi qua Zod.
- Nếu hợp lệ, thì bạn sẽ sử dụng dữ liệu đó.
- Nếu không, bạn sẽ trả về lỗi, yêu cầu người dùng nhập lại.
Zod cũng tích hợp rất ngon với TypeScript, cho phép bạn suy luận kiểu (type inference) trực tiếp từ schema mà không phải viết đi viết lại kiểu bằng tay.
2. Tại sao nên dùng Zod trong dự án Next.js?
Next.js rất phổ biến và thường được sử dụng với TypeScript. Khi kết hợp Zod, bạn có thể:
- Dễ dàng xác thực dữ liệu ngay trong API routes.
- Chia sẻ schema giữa client (frontend) và server (backend) trong cùng một codebase.
- Loại bỏ nhiều lỗi runtime vì dữ liệu luôn được kiểm tra cẩn thận.
Loại bỏ "if/else" kiểm tra dữ liệu rườm rà. Thay vì bạn phải code như thế này:
if (!email || typeof email !== 'string' || !email.includes('@')) {
return res.status(400).json({error: "Email không hợp lệ"});
}
if (!password || typeof password !== 'string' || password.length < 6) {
return res.status(400).json({error: "Mật khẩu phải >= 6 ký tự"});
}
Bạn chỉ cần định nghĩa một schema và parse nó. Việc check Zod sẽ đảm nhận, code trở nên rõ ràng và ngắn gọn hơn nhiều.
Dễ dàng tích hợp với React Hook Form (hoặc các thư viện form khác)
Zod có thể kết hợp với React Hook Form thông qua @hookform/resolvers/zod
. Nhờ đó, bạn xác thực form ngay ở phía client, hiển thị lỗi cho người dùng real-time mà không cần chờ đến lúc người dùng submit.
3. Những khái niệm cơ bản trong Zod
3.1 Schema là gì?
Schema trong Zod giống như một "bản vẽ" dữ liệu. Bạn nói: "Tôi muốn một object có trường email là chuỗi email hợp lệ, password là chuỗi >= 6 ký tự". Dựa trên schema, Zod sẽ kiểm tra dữ liệu thực tế.
Ví dụ một schema đơn giản cho việc đăng ký tài khoản (gồm email, password, name)
import { z } from 'zod';
export const registerSchema = z.object({
email: z.string().email("Email không hợp lệ"),
password: z.string().min(8, "Mật khẩu phải ≥ 8 ký tự"),
name: z.string().nonempty("Họ tên không được trống"),
});
Bây giờ, thử parse một object:
const formData = {
email: "ttv@gmail.com",
password: "12345678",
name: "Tran Vy"
};
const parsedData = registerSchema.parse(formData);
console.log(parsedData);
Khi bạn chạy chương trình (giả sử trong môi trường node hoặc Next.js API), bạn sẽ nhận được parsedData đúng với kiểu bạn mong muốn. Nếu thử email: "abc" (không phải email hợp lệ), Zod sẽ ném lỗi kèm thông báo: Email không hợp lệ.
3.2 Một số kiểu dữ liệu cơ bản
z.string()
cho chuỗiz.number()
cho sốz.boolean()
cho booleanz.object({...})
cho objectz.array(subSchema)
cho mảngz.enum([...])
cho enum- Và nhiều kiểu khác như là: date, literal, union, intersection...)
3.3 parse and safeParse
schema.parse(data)
: nếu dữ liệu sai, nó sẽ throw lỗi (throw error).schema.safeParse(data)
: không ném lỗi, mà trả về một object { success: boolean; data?: T; error?: ZodError }.
Cá nhân mình thấy, nếu mà bạn đang ở phía server (API route), safeParse thường tiện hơn vì bạn có thể xử lý lỗi mà không cần phải try/catch.
3.4 Suy luận kiểu (type inference)
Bạn có thể lấy kiểu TypeScript từ schema bằng z.infer
type User = z.infer<typeof userSchema>;
// user sẽ có kiểu: {email: string; password: string;}
Điều này giúp bạn không phải viết lại interface hay type cách thủ công.
Hoặc bạn có thể định nghĩa Schema bằng Zod, suy ra kiểu TypeScript tương ứng và dùng nó ở bất kỳ đâu
const userSchema = z.object({
id: z.number(),
email: z.string().email(),
});
type User = z.infer<typeof userSchema>;
function printUser(u: User) {
console.log(u.id, u.email);
}
u ở đây chắc chắn phải có id: number
và email: string
. Bạn không phải lo lắng về việc id là kiểu string hay email bị để trống. Mọi thứ được đảm bảo bởi Zod ở runtime và xác nhận bởi TypeScript ở compile-time.
3.5 Refine và Transform
Zod không chỉ dừng lại ở việc check kiểu dữ liệu. Bạn còn có thể thêm logic phức tạp hơn bằng refine, hoặc thay đổi dữ liệu sau khi parse bằng transform.
Ví dụ, bạn có 2 trường password và confirmPassword. Bạn muốn chắc chắn rằng chúng giống nhau. Bạn có thể dùng refine:
const passwordSchema = z.object({
password: z.string().min(6),
confirmPassword: z.string()
}).refine(data => data.password === data.confirmPassword, {
message: "not match",
path: ["confirmPassword"],
});
Với transform bạn có thể biến đổi dữ liệu sau khi xác thực. Ví dụ, bạn muốn email luôn ở dạng lowercase:
const emailSchema = z.string().email().transform(str => str.toLowerCase());
Sau khi parse, email trả về chắc chắn lowercase. Điều này tiện nếu bạn muốn normalize dữ liệu đầu vào.
4. Tích hợp Zod vào API Routes Nextjs
Trong Next.js, bạn có thư mục pages/api
để tạo API routes. Ví dụ, bạn tạo file pages/api/register.ts
import { NextApiRequest, NextApiResponse } from 'next';
import { registerSchema } from '../../lib/schemas/register-schema';
// --------------------------------------------------------------------
export default function handler(req: NextApiRequest, res: NextApiResponse) {
if (req.method !== "POST") {
return res.status(405).json({error: "Method not allowed"});
}
// req.body sẽ là object chứa data từ client (client gửi JSON)
const parsed = registerSchema.safeParse(req.body);
if (!parsed.success) {
return res.status(400).json({
error: "Dữ liệu không hợp lệ",
details: parsed.error.format()
});
}
const { email, password, name } = parsed.data;
return res.status(200).json({message: "Đăng ký thành công!"});
}
Khi client gửi request POST với dữ liệu hợp lệ, bạn trả về 200. Nếu dữ liệu sai, trả về 400 kèm chi tiết lỗi. Việc check dữ liệu gọn gàng hơn nhiều so với dùng if/else phải không?
Cách bên trên là sử dụng với Page Router, ở Nextjs 13 trở lên bạn có thể dùng App Router. App Router cho phép bạn định nghĩa route API dưới dạng file route.ts (hoặc route.js) trong folder app/. Việc xử lý request/response cũng khác một chút: bạn sẽ làm việc với Request Web API (chuẩn của browser/Node) và trả về Response.
file app/api/register/route.ts
import { NextResponse } from 'next/server';
import { registerSchema } from '@/lib/schemas/register-schema';
// ------------------------------------------------------------
export async function POST(request: Request) {
const body = await request.json();
const parsed = registerSchema.safeParse(body);
if (!parsed.success) {
return NextResponse.json({
error: "Dữ liệu không hợp lệ",
details: parsed.error.format()
}, { status: 400 });
}
const { email, password, name } = parsed.data;
return NextResponse.json({ message: "Đăng ký thành công!" }, { status: 200 });
}
- Không dùng NextApiRequest, NextApiResponse: Với App Router, hàm xử lý request là các hàm GET, POST, PUT, DELETE… tương ứng, chứ không phải là một hàm duy nhất export default như trước.
- Dùng Request Web API: lúc này request là một instance của Request (theo Web API). Để lấy dữ liệu JSON từ body, bạn gọi await request.json().
- Trả về Response hoặc NextResponse: thay vì res.status(200).json(...), bạn dùng NextResponse.json(data, { status: ... }) để trả về JSON response. NextResponse được Next.js cung cấp giúp việc tạo response dễ dàng hơn. Bạn cũng có thể dùng new Response(JSON.stringify(...), {status: ...}) nếu muốn.
- Kiểm tra method: vì bạn đặt hàm POST nên route này mặc định xử lý method POST. Nếu bạn muốn hỗ trợ nhiều method, bạn có thể export nhiều hàm tương ứng như export async function GET(...) { ... }, export async function POST(...) { ... },...
5. Xác thực dữ liệu trên Frontend (trong component React)
Khi xây dựng form đăng ký trên frontend (trong Next.js), bạn có thể kết hợp Zod với React Hook Form - một thư viện form phổ biến, nhẹ, dễ dùng.
import React from 'react';
import { useForm } from 'react-hook-form';
import { zodResolver } from '@hookform/resolvers/zod';
import { registerSchema } from '../lib/schemas/register-schema';
import { z } from 'zod';
// -------------------------------------------------------------
type RegisterFormData = z.infer<typeof registerSchema>;
export default function RegisterPage() {
const { register, handleSubmit, formState: { errors } } = useForm<RegisterFormData>({
resolver: zodResolver(registerSchema)
});
const onSubmit = async (data: RegisterFormData) => {
const res = await fetch('/api/register', {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify(data),
});
if (!res.ok) {
const errorData = await res.json();
console.error("Lỗi từ server:", errorData);
return;
}
alert("Đăng ký thành công!");
};
return (
<form onSubmit={handleSubmit(onSubmit)}>
<div>
<label>Email: </label>
<input type="text" {...register('email')} />
{errors.email && <p style={{color: 'red'}}>{errors.email.message}</p>}
</div>
<div>
<label>Mật khẩu: </label>
<input type="password" {...register('password')} />
{errors.password && <p style={{color: 'red'}}>{errors.password.message}</p>}
</div>
<div>
<label>Họ tên: </label>
<input type="text" {...register('name')} />
{errors.name && <p style={{color: 'red'}}>{errors.name.message}</p>}
</div>
<button type="submit">Đăng ký</button>
</form>
);
}
Ở đây, zodResolver sẽ dùng schema registerSchema để xác thực dữ liệu form trước khi gửi lên server. Nếu email không hợp lệ hoặc password quá ngắn, lỗi sẽ hiển thị ngay lập tức bên dưới trường input tương ứng.
Điều này mang lại trải nghiệm tốt cho người dùng: họ biết sai ở đâu và sửa ngay, thay vì gửi form xong mới nhận về thông báo lỗi.
6. Một vài lời khuyên khi sử dụng Zod
- Tách schemas ra thành file riêng: như ví dụ ở trên, cho schema vào lib/schemas/ để dễ tìm, dễ quản lý.
- Tái sử dụng schema giữa client và server: đừng định nghĩa lại kiểu dữ liệu ở nhiều nơi. Chỉ cần 1 schema duy nhất, import và dùng cả ở frontend lẫn backend.
- Sử dụng TypeScript inference: luôn
type MyType = z.infer<typeof mySchema>
để tránh sai sót và thừa. - Sử dụng refine và transform khi cần: khi logic phức tạp hơn, hãy tận dụng refine. Khi cần chuẩn hóa dữ liệu (VD: trim chuỗi, lowercase email), hãy dùng transform.
- Xử lý lỗi gọn gàng: trả về lỗi dạng JSON rõ ràng cho client. Người dùng muốn biết họ sai ở đâu để sửa mà.
7. Kết luận
Zod không chỉ hữu ích trong dự án Next.js mà còn dùng tốt ở các dự án Node.js, React, hay bất cứ nơi nào bạn cần xác thực dữ liệu. Hãy thử áp dụng nó vào project của bạn, bạn sẽ thấy code nhìn clean hơn, yên tâm hơn khi dữ liệu được kiểm soát chặt chẽ.
Qua bài này, hy vọng bạn đã hiểu rõ về khái niệm cũng như các chức năng, sử dụng Zod trong dự án thế nào.
Các bài viết liên quan:
Bài viết liên quan
Tìm hiểu Microfrontend: Hướng dẫn triển khai Microfrontend trong Next.js
Dec 24, 2024 • 6 min read
Yup là gì? Hướng dẫn Validation với Yup trong dự án React
Dec 19, 2024 • 14 min read
Giới thiệu Ant Design: Hệ thống thiết kế UI dành cho Website
Dec 18, 2024 • 10 min read
Hướng dẫn tích hợp Sentry vào ứng dụng React
Dec 18, 2024 • 7 min read
MUI (Material UI): Công cụ rút ngắn thời gian xây dựng Giao diện
Dec 18, 2024 • 11 min read
Tìm hiểu Sentry: Công cụ Theo dõi Lỗi và Hiệu suất tự động
Dec 16, 2024 • 7 min read