الخطوات
-
1
ثبّت Zod
Zod ليس لها تبعيات وقت تشغيل وتعمل مع JavaScript العادي أو TypeScript. ثبّتها مرة واحدة واستخدمها في كامل المشروع.
bashnpm install zod -
2
عرّف أول schema
تصف Zod schema الشكل والقيود التي تتوقعها تمامًا. أعلنها في أعلى الملف (أو في مجلد
schemas/مخصص) حتى يمكن مشاركة نفس الـ schema بين معالج المسار وأي اختبارات.typescript// src/schemas/user.schema.ts (أو .js) import { z } from "zod" export const createUserSchema = z.object({ name: z.string().min(2).max(100).trim(), email: z.string().email().toLowerCase(), age: z.number().int().min(18).max(120), role: z.enum(["admin", "user"]).default("user"), }) // TypeScript: استنتج النوع — لا حاجة لـ interface منفصل export type CreateUserInput = z.infer<typeof createUserSchema> // { name: string; email: string; age: number; role: "admin" | "user" } -
3
استخدم safeParse للتحقق من طلب
schema.parse()يرمي استثناءً عند الفشل.schema.safeParse()يُعيد كائن نتيجة بدلًا من ذلك — استخدمه في معالجات المسار حتى تتحكم في الاستجابة. عند النجاح،result.dataهي القيمة المحللة المُنظّفة (سلاسل مُقلّصة، قيم افتراضية مُطبّقة). عند الفشل،result.errorتحتوي القائمة الكاملة للمشكلات.typescript// src/controllers/usersController.ts import { createUserSchema } from "../schemas/user.schema" export const create = (req: Request, res: Response) => { const result = createUserSchema.safeParse(req.body) if (!result.success) { return res.status(422).json({ error: "Validation failed", issues: result.error.issues.map((issue) => ({ field: issue.path.join("."), message: issue.message, })), }) } // result.data مكتوب بأنواع كاملة وآمن للاستخدام const { name, email, age, role } = result.data // ... احفظ في قاعدة البيانات res.status(201).json({ name, email, age, role }) } -
4
أعد أخطاء تحقق منظّمة
يحتاج العملاء تفاصيل أخطاء قابلة للقراءة آليًا، لا مجرد 422 عام. الصيغة أدناه تعطي كل مشكلة مسار حقلها ورسالة بشرية — سهلة العرض بجانب حقول النماذج مباشرةً.
json// مثال على body الاستجابة عند فشل التحقق // POST /api/users {"name":"A","email":"not-an-email","age":15} { "error": "Validation failed", "issues": [ { "field": "name", "message": "String must contain at least 2 character(s)" }, { "field": "email", "message": "Invalid email" }, { "field": "age", "message": "Number must be greater than or equal to 18" } ] } -
5
استخرج middleware تحقق قابل للإعادة
وضع استدعاءات
safeParseمباشرةً في كل controller متكرر. اكتب مصنع middleware من الدرجة العليا يأخذ schema ويُعيد middleware — المسارات تبقى نظيفة والتحقق يصبح تصريحيًا.typescript// src/middleware/validate.ts import { ZodSchema } from "zod" import { Request, Response, NextFunction } from "express" export function validate(schema: ZodSchema) { return (req: Request, res: Response, next: NextFunction) => { const result = schema.safeParse(req.body) if (!result.success) { return res.status(422).json({ error: "Validation failed", issues: result.error.issues.map((issue) => ({ field: issue.path.join("."), message: issue.message, })), }) } req.body = result.data // استبدل body الخام بالبيانات المحللة والمُنظّفة next() } } // الاستخدام في ملف route: import { validate } from "../middleware/validate" import { createUserSchema } from "../schemas/user.schema" router.post("/", validate(createUserSchema), usersController.create) -
6
نظّف السلاسل داخل الـ schema
تعمل تحولات Zod أثناء التحليل، لذا التنظيف مدمج في الـ schema — لا مبعثر عبر controllers. قلّص المسافات البيضاء، وحدّد الحالة، وفرض حدود الطول في مكان واحد. القيمة في
req.bodyبعدsafeParseنظيفة بالفعل.typescriptexport const createPostSchema = z.object({ title: z .string() .min(5, "Title must be at least 5 characters") .max(200, "Title cannot exceed 200 characters") .trim(), // أزل المسافات البيضاء من الطرفين slug: z .string() .toLowerCase() // وحّد الحالة قبل التخزين .regex(/^[a-z0-9-]+$/, "Slug can only contain lowercase letters, numbers, and hyphens"), content: z .string() .min(20) .max(50_000), // امنع payloads بحجم ميغابايتات published: z.boolean().default(false), tags: z .array(z.string().trim().max(50)) .max(10, "No more than 10 tags allowed") .default([]), }) -
7
تحقق من params المسار وquery strings
Zod لا تقتصر على request bodies. تحقق من معاملات المسار (التي تكون دائمًا سلاسل في Express) وquery strings بنفس الطريقة. حوّل الأنواع صراحةً —
req.params.idتأتي كسلسلة ويجب تحويلها إلى رقم.typescript// التحقق من params المسار const idParamSchema = z.object({ id: z.coerce.number().int().positive(), // يحول "42" → 42 }) // التحقق من query strings const listQuerySchema = z.object({ page: z.coerce.number().int().positive().default(1), limit: z.coerce.number().int().min(1).max(100).default(20), search: z.string().trim().max(100).optional(), }) // middleware التحقق موسّع للـ params: export function validateParams(schema: ZodSchema) { return (req: Request, res: Response, next: NextFunction) => { const result = schema.safeParse(req.params) if (!result.success) { return res.status(400).json({ error: "Invalid URL parameter", issues: result.error.issues }) } req.params = result.data next() } }
نصائح ومحاذير
- استخدم `z.coerce.number()` بدلًا من `z.number()` لأي قيمة قد تصل كسلسلة (params المسار، query strings). يحول `"42"` إلى `42` قبل التحقق.
- ضع schemas في مجلد `src/schemas/` واستوردها في كل من معالجات المسار والاختبارات. الـ schema تُضاعف كتوثيق لعقد API المتوقع.
- استخدم `z.discriminatedUnion()` عندما يمكن أن يكون لـ request body أشكال مختلفة حسب حقل `type` — أسرع ويعطي رسائل خطأ أفضل من `z.union()` العادي.
- `result.error.flatten()` بديل مناسب لـ `.issues` يجمع الأخطاء حسب الحقل: `{ fieldErrors: { email: ["Invalid email"] }, formErrors: [] }` — مفيد لاستجابات التحقق من النماذج.
خاتمة
يحوّل Zod التحقق من المدخلات من فكرة لاحقة إلى جزء أساسي من عقد API. عرّف الـ schema مرة واحدة، احصل على الأمان في وقت التشغيل وأنواع TypeScript مجانًا، وعالج الأخطاء في middleware واحد بدلًا من تبعثر الفحوصات عبر كل معالج. مقترنًا بنمط middleware التحقق، كل مسار في المشروع يحصل على استجابات أخطاء متسقة ومنظّمة بأقل كود ممكن لكل مسار.