Zod چیست؟ آموزش کامل اعتبارسنجی داده در TypeScript، React و Node.js

در این آموزش Zod را از صفر یاد می‌گیرید و با Schema، parse، safeParse، اعتبارسنجی فرم React، ورودی Express، متغیرهای محیطی و پاسخ API هوش مصنوعی یک سیستم Type-Safe و قابل‌اعتماد می‌سازید.

Share
Zod چیست؟ آموزش کامل اعتبارسنجی داده در TypeScript، React و Node.js

TypeScript می‌تواند بسیاری از خطاهای برنامه‌نویسی را پیش از اجرای برنامه پیدا کند؛ اما توانایی TypeScript در زمان Compile به پایان می‌رسد. وقتی داده از فرم، API، فایل JSON، متغیر محیطی، دیتابیس یا یک مدل هوش مصنوعی وارد برنامه می‌شود، TypeScript نمی‌تواند تضمین کند ساختار واقعی داده با Type تعریف‌شده هماهنگ است.

فرض کنید این Type را تعریف کرده‌ایم:

type User = {
  name: string;
  age: number;
};

سپس داده‌ای از یک API دریافت می‌کنیم:

const response = await fetch("/api/user");
const user: User = await response.json();

نوشتن : User داده را اعتبارسنجی نمی‌کند. اگر API مقدار زیر را برگرداند، TypeScript در زمان اجرا مانع آن نخواهد شد:

{
  "name": 123,
  "age": "twenty"
}

اینجاست که Zod وارد می‌شود. Zod به ما اجازه می‌دهد ساختار داده را به شکل یک Schema تعریف کنیم، داده واقعی را در Runtime بررسی کنیم و هم‌زمان Typeهای TypeScript را از همان Schema استخراج کنیم.

در این مقاله Zod را به‌صورت عملی در پروژه‌های TypeScript، React، Node.js و Express پیاده‌سازی می‌کنیم و نشان می‌دهیم چگونه می‌توان ورودی کاربران، پاسخ APIها، متغیرهای محیطی و خروجی مدل‌های هوش مصنوعی را اعتبارسنجی کرد.

Zod چیست؟

Zod یک کتابخانه TypeScript-First برای تعریف Schema و اعتبارسنجی داده در زمان اجرا است. با استفاده از Zod می‌توان از یک رشته ساده تا Objectهای تو‌در‌تو، Arrayها، Unionها و ساختارهای پیچیده را بررسی کرد.

براساس مستندات رسمی Zod، این کتابخانه علاوه بر Runtime Validation از استخراج خودکار Typeهای TypeScript نیز پشتیبانی می‌کند.

یک مثال ساده:

import { z } from "zod";

const UserSchema = z.object({
  name: z.string(),
  age: z.number(),
});

const input = {
  name: "Sara",
  age: 28,
};

const user = UserSchema.parse(input);

console.log(user.name);

در این مثال:

  • UserSchema قرارداد معتبر بودن داده را تعریف می‌کند.
  • parse داده ورودی را بررسی می‌کند.
  • اگر داده معتبر باشد، مقدار اعتبارسنجی‌شده برگردانده می‌شود.
  • اگر داده معتبر نباشد، Zod خطا ایجاد می‌کند.
  • Type خروجی به‌صورت خودکار توسط TypeScript شناخته می‌شود.

چرا TypeScript به‌تنهایی کافی نیست؟

TypeScript یک سیستم نوع‌دهی ایستا یا Static Type System است. Typeها هنگام Build و بررسی کد استفاده می‌شوند و معمولاً در JavaScript خروجی وجود ندارند.

برای مثال:

type Product = {
  title: string;
  price: number;
};

const product = JSON.parse(
  '{"title":"Keyboard","price":"invalid"}',
) as Product;

استفاده از as Product فقط به TypeScript می‌گوید به برنامه‌نویس اعتماد کند. این عبارت مقدار "invalid" را به عدد تبدیل نمی‌کند و صحت داده را نیز بررسی نمی‌کند.

اگر کد زیر اجرا شود:

console.log(product.price.toFixed(2));

برنامه در Runtime با خطا روبه‌رو خواهد شد؛ زیرا price در واقع یک رشته است.

با Zod می‌توان داده را پیش از استفاده بررسی کرد:

import { z } from "zod";

const ProductSchema = z.object({
  title: z.string(),
  price: z.number(),
});

const rawData: unknown = JSON.parse(
  '{"title":"Keyboard","price":"invalid"}',
);

const product = ProductSchema.parse(rawData);

اکنون برنامه پیش از استفاده از داده نامعتبر متوقف می‌شود و خطایی ساختاریافته در اختیار توسعه‌دهنده قرار می‌گیرد.

TypeScript Type و Zod Schema چه تفاوتی دارند؟

ویژگیTypeScript TypeZod Schema
زمان بررسیCompile TimeRuntime
اعتبارسنجی پاسخ APIندارددارد
اعتبارسنجی فرمندارددارد
تولید پیام خطاندارددارد
تبدیل دادهندارددارد
استخراج Typeتعریف مستقیمبا z.infer
حضور در JavaScript نهاییمعمولاً حذف می‌شوددر Runtime اجرا می‌شود

در یک پروژه حرفه‌ای، TypeScript و Zod جایگزین یکدیگر نیستند. TypeScript کد داخلی برنامه را Type-Safe می‌کند و Zod مرزهای ورود داده را کنترل می‌کند.

چه داده‌هایی باید با Zod اعتبارسنجی شوند؟

هر داده‌ای که از خارج محدوده کنترل مستقیم برنامه وارد می‌شود، می‌تواند گزینه مناسبی برای اعتبارسنجی باشد:

  • req.body، req.query و req.params در API
  • داده فرم‌های React
  • پاسخ APIهای داخلی و خارجی
  • خروجی مدل‌های هوش مصنوعی
  • محتوای فایل‌های JSON
  • متغیرهای محیطی
  • Webhookها
  • داده Local Storage
  • Query String مرورگر
  • داده دریافت‌شده از Message Queue
  • تنظیمات برنامه
  • ورودی ابزارهای AI Agent
  • خروجی Function Calling و Structured Output

اعتبارسنجی به معنی بی‌اعتماد بودن به تمام سرویس‌ها نیست. هدف این است که مرزهای سیستم به‌صورت صریح و قابل پیش‌بینی کنترل شوند.

نصب Zod

برای نصب Zod در پروژه npm اجرا کنید:

npm install zod

در pnpm:

pnpm add zod

در Yarn:

yarn add zod

سپس آن را وارد پروژه کنید:

import { z } from "zod";

در نسخه‌های جدید Zod، نسخه ۴ پایدار است. پیش از ارتقای یک پروژه قدیمی از Zod 3، راهنمای مهاجرت Zod 4 را بررسی کنید؛ زیرا بعضی APIها، به‌خصوص روش سفارشی‌سازی خطاها، تغییر کرده‌اند.

همچنین بهتر است strict در فایل tsconfig.json فعال باشد:

{
  "compilerOptions": {
    "strict": true,
    "target": "ES2022",
    "module": "NodeNext",
    "moduleResolution": "NodeNext"
  }
}

ساخت اولین Schema در Zod

یک Schema ساده کاربر:

import { z } from "zod";

const UserSchema = z.object({
  id: z.string(),
  name: z.string(),
  email: z.email(),
  age: z.number(),
  active: z.boolean(),
});

داده معتبر:

const user = UserSchema.parse({
  id: "usr_123",
  name: "Sara Ahmadi",
  email: "sara@example.com",
  age: 29,
  active: true,
});

داده نامعتبر:

UserSchema.parse({
  id: 123,
  name: "Sara Ahmadi",
  email: "not-an-email",
  age: "29",
  active: true,
});

در حالت دوم، Zod خطایی شامل مسیر فیلدها، نوع مورد انتظار و نوع دریافت‌شده ایجاد می‌کند.

استخراج Type از Schema با z.infer

یکی از مهم‌ترین مزیت‌های Zod این است که لازم نیست Schema و Type را جداگانه تعریف کنید.

import { z } from "zod";

const UserSchema = z.object({
  id: z.string(),
  name: z.string(),
  email: z.email(),
  age: z.number().int().min(18),
});

type User = z.infer<typeof UserSchema>;

Type استخراج‌شده تقریباً معادل این ساختار است:

type User = {
  id: string;
  name: string;
  email: string;
  age: number;
};

اکنون Schema منبع اصلی حقیقت یا Single Source of Truth است. هر تغییری در Schema به‌صورت خودکار روی Type نیز اثر می‌گذارد.

این روش از ناهماهنگی زیر جلوگیری می‌کند:

type User = {
  email: string;
  age: string;
};

const UserSchema = z.object({
  email: z.email(),
  age: z.number(),
});

تفاوت parse و safeParse

Zod دو روش اصلی برای اعتبارسنجی داده دارد:

استفاده از parse

اگر داده نامعتبر باشد، parse یک ZodError ایجاد می‌کند:

const user = UserSchema.parse(input);

این روش زمانی مناسب است که نامعتبر بودن داده یک وضعیت استثنایی محسوب می‌شود و قرار است خطا در سطح بالاتری مدیریت شود.

try {
  const user = UserSchema.parse(input);
  console.log(user);
} catch (error) {
  console.error("Invalid user data", error);
}

استفاده از safeParse

safeParse خطا پرتاب نمی‌کند و یک نتیجه قابل بررسی برمی‌گرداند:

const result = UserSchema.safeParse(input);

if (!result.success) {
  console.log(result.error);
} else {
  console.log(result.data);
}

در صورت موفقیت:

{
  success: true,
  data: validatedData
}

در صورت شکست:

{
  success: false,
  error: ZodError
}

برای فرم‌ها، Routeهای API و فرایندهایی که داده نامعتبر یک نتیجه قابل انتظار است، safeParse معمولاً انتخاب مناسبی محسوب می‌شود.

اعتبارسنجی رشته‌ها

Zod امکانات مختلفی برای بررسی رشته‌ها ارائه می‌کند:

const ProfileSchema = z.object({
  username: z.string().min(3).max(30),
  email: z.email(),
  website: z.url().optional(),
  bio: z.string().max(500).optional(),
  nationalCode: z.string().regex(/^\d{10}$/),
});

می‌توان ورودی را Trim نیز کرد:

const NameSchema = z.string().trim().min(2).max(100);

مثال:

const name = NameSchema.parse("  Amir  ");

console.log(name);

خروجی:

Amir

اعتبارسنجی اعداد

const PriceSchema = z.number().positive();
const AgeSchema = z.number().int().min(18).max(120);
const DiscountSchema = z.number().min(0).max(100);

نمونه Object:

const ProductSchema = z.object({
  title: z.string().min(2),
  price: z.number().nonnegative(),
  inventory: z.number().int().nonnegative(),
  discountPercent: z.number().min(0).max(100).optional(),
});

z.number() رشته عددی را به‌صورت خودکار قبول نمی‌کند:

z.number().parse("42");

این داده نامعتبر است؛ زیرا "42" یک رشته است.

اگر تبدیل کنترل‌شده لازم باشد، می‌توان از Coercion استفاده کرد:

const PageSchema = z.coerce.number().int().min(1);

اکنون:

PageSchema.parse("3");

خروجی عدد 3 خواهد بود.

Coercion باید آگاهانه استفاده شود؛ زیرا تبدیل خودکار بعضی ورودی‌ها ممکن است رفتاری غیرمنتظره ایجاد کند.

optional، nullable و nullish

این سه مفهوم تفاوت مهمی دارند.

optional

فیلد می‌تواند وجود نداشته باشد یا undefined باشد:

const Schema = z.object({
  bio: z.string().optional(),
});

nullable

فیلد باید وجود داشته باشد، اما می‌تواند null باشد:

const Schema = z.object({
  avatar: z.url().nullable(),
});

nullish

فیلد می‌تواند وجود نداشته باشد، undefined یا null باشد:

const Schema = z.object({
  middleName: z.string().nullish(),
});

انتخاب نادرست میان این حالت‌ها یکی از دلایل رایج ناهماهنگی Frontend، Backend و دیتابیس است.

مقدار پیش‌فرض با default

می‌توان برای داده‌های اختیاری مقدار پیش‌فرض تعیین کرد:

const SettingsSchema = z.object({
  language: z.string().default("fa"),
  theme: z.enum(["light", "dark"]).default("light"),
  pageSize: z.number().int().min(10).max(100).default(20),
});

اگر ورودی خالی باشد:

const settings = SettingsSchema.parse({});

خروجی:

{
  language: "fa",
  theme: "light",
  pageSize: 20
}

Array، Tuple و Record در Zod

Array

const TagsSchema = z.array(z.string().min(1)).min(1).max(10);

Object شامل Array:

const ArticleSchema = z.object({
  title: z.string().min(5),
  tags: z.array(z.string()).max(10),
});

Tuple

برای آرایه‌ای با تعداد و ترتیب مشخص:

const CoordinateSchema = z.tuple([
  z.number(),
  z.number(),
]);

داده معتبر:

[35.7219, 51.3347]

Record

برای Objectهایی با کلیدهای پویا:

const ScoresSchema = z.record(
  z.string(),
  z.number().min(0).max(100),
);

داده معتبر:

{
  coding: 92,
  reasoning: 88,
  speed: 76
}

Enum در Zod

برای مقادیر محدود می‌توان از z.enum استفاده کرد:

const RoleSchema = z.enum([
  "admin",
  "developer",
  "viewer",
]);

type Role = z.infer<typeof RoleSchema>;

در Schema:

const UserSchema = z.object({
  name: z.string(),
  role: RoleSchema,
});

مقدار زیر نامعتبر خواهد بود:

{
  "name": "Sara",
  "role": "owner"
}

زیرا owner در فهرست مقادیر مجاز وجود ندارد.

Union و Discriminated Union

گاهی داده می‌تواند یکی از چند ساختار مختلف باشد.

Union ساده

const IdentifierSchema = z.union([
  z.string(),
  z.number(),
]);

یا با Syntax کوتاه‌تر:

const IdentifierSchema = z.string().or(z.number());

Discriminated Union

برای پاسخ‌هایی که یک فیلد مشخص‌کننده دارند، z.discriminatedUnion خواناتر و دقیق‌تر است:

const ApiResultSchema = z.discriminatedUnion("status", [
  z.object({
    status: z.literal("success"),
    data: z.object({
      id: z.string(),
      message: z.string(),
    }),
  }),
  z.object({
    status: z.literal("error"),
    error: z.string(),
  }),
]);

داده موفق:

{
  "status": "success",
  "data": {
    "id": "req_123",
    "message": "عملیات انجام شد"
  }
}

داده ناموفق:

{
  "status": "error",
  "error": "درخواست معتبر نیست"
}

پس از بررسی status، TypeScript ساختار دقیق داده را تشخیص می‌دهد:

const result = ApiResultSchema.parse(input);

if (result.status === "success") {
  console.log(result.data.message);
} else {
  console.log(result.error);
}

ترکیب و استفاده مجدد از Schemaها

برای جلوگیری از تکرار می‌توان Schemaهای کوچک‌تر ساخت:

const BaseUserSchema = z.object({
  id: z.string(),
  name: z.string().min(2),
  email: z.email(),
});

const AdminSchema = BaseUserSchema.extend({
  role: z.literal("admin"),
  permissions: z.array(z.string()),
});

pick

انتخاب تعدادی از فیلدها:

const UserPreviewSchema = BaseUserSchema.pick({
  id: true,
  name: true,
});

omit

حذف تعدادی از فیلدها:

const PublicUserSchema = BaseUserSchema.omit({
  email: true,
});

partial

اختیاری کردن تمام فیلدها:

const UpdateUserSchema = BaseUserSchema.partial();

این الگو برای Endpointهای Update کاربردی است، اما باید مشخص کنید کدام فیلدها واقعاً اجازه تغییر دارند.

required

اجباری کردن فیلدهای اختیاری:

const RequiredUserSchema = UpdateUserSchema.required();

اعتبارسنجی‌های وابسته به چند فیلد با refine

گاهی اعتبار یک فیلد به فیلد دیگری وابسته است. برای مثال، تأیید رمز عبور:

const RegisterSchema = z
  .object({
    email: z.email(),
    password: z.string().min(8),
    confirmPassword: z.string().min(8),
  })
  .refine(
    (data) => data.password === data.confirmPassword,
    {
      error: "رمز عبور و تکرار آن یکسان نیستند.",
      path: ["confirmPassword"],
    },
  );

نمونه دیگر، بازه زمانی:

const DateRangeSchema = z
  .object({
    startDate: z.coerce.date(),
    endDate: z.coerce.date(),
  })
  .refine(
    (data) => data.endDate >= data.startDate,
    {
      error: "تاریخ پایان نباید قبل از تاریخ شروع باشد.",
      path: ["endDate"],
    },
  );

در Zod 4، پارامتر error روش یکپارچه سفارشی‌سازی خطاها است. جزئیات بیشتر در مستندات Customizing Errors آمده است.

تبدیل داده با transform

Zod می‌تواند علاوه بر اعتبارسنجی، داده را تبدیل کند:

const NormalizedEmailSchema = z
  .email()
  .transform((email) => email.trim().toLowerCase());

مثال:

const email = NormalizedEmailSchema.parse(
  "User@Example.com",
);

console.log(email);

خروجی:

user@example.com

نمونه تبدیل رشته جداشده با ویرگول به Array:

const TagsSchema = z
  .string()
  .transform((value) =>
    value
      .split(",")
      .map((tag) => tag.trim())
      .filter(Boolean),
  );

استفاده از Transform باید شفاف باشد. اگر Schema تغییر شکل قابل توجهی در داده ایجاد می‌کند، بهتر است نام آن این رفتار را نشان دهد.

تفاوت Type ورودی و خروجی

وقتی از Transform استفاده می‌شود، Type ورودی و خروجی یکسان نیستند:

const PortSchema = z
  .string()
  .transform((value) => Number(value));

Type ورودی:

type PortInput = z.input<typeof PortSchema>;

معادل:

type PortInput = string;

Type خروجی:

type PortOutput = z.output<typeof PortSchema>;

معادل:

type PortOutput = number;

این تفاوت در فرم‌ها، Query Stringها و متغیرهای محیطی اهمیت زیادی دارد.

مدیریت خطاهای Zod

یک ZodError شامل مجموعه‌ای از Issueها است. هر Issue اطلاعاتی مانند مسیر فیلد، کد خطا و پیام دارد.

const result = UserSchema.safeParse(input);

if (!result.success) {
  console.log(result.error.issues);
}

نمونه خروجی مفهومی:

[
  {
    code: "invalid_type",
    path: ["age"],
    message: "Invalid input",
  },
];

برای فرم‌های ساده می‌توان خطاها را Flat کرد:

const result = RegisterSchema.safeParse(input);

if (!result.success) {
  const errors = z.flattenError(result.error);

  console.log(errors.fieldErrors);
  console.log(errors.formErrors);
}

مستندات Zod ابزارهایی مانند z.flattenError، z.treeifyError و z.prettifyError را برای نمایش خطاها معرفی می‌کند. برای ساختارهای به‌روز به راهنمای Formatting Errors مراجعه کنید.

پیام خطای فارسی در Zod

می‌توان پیام هر قاعده را به فارسی تعریف کرد:

const LoginSchema = z.object({
  email: z.email({
    error: "آدرس ایمیل معتبر نیست.",
  }),
  password: z.string().min(8, {
    error: "رمز عبور باید حداقل ۸ کاراکتر باشد.",
  }),
});

برای فیلد نام:

const NameSchema = z
  .string({
    error: "نام باید به‌صورت متن ارسال شود.",
  })
  .trim()
  .min(2, {
    error: "نام باید حداقل ۲ کاراکتر باشد.",
  })
  .max(100, {
    error: "نام نمی‌تواند بیشتر از ۱۰۰ کاراکتر باشد.",
  });

پیام خطا باید برای کاربر قابل فهم باشد، اما نباید اطلاعات داخلی، Stack Trace، مسیر فایل‌ها یا جزئیات حساس سیستم را افشا کند.

اعتبارسنجی فرم React با Zod

می‌توان Zod را بدون کتابخانه اضافی در فرم React استفاده کرد.

import { FormEvent, useState } from "react";
import { z } from "zod";

const ContactSchema = z.object({
  name: z
    .string()
    .trim()
    .min(2, { error: "نام حداقل باید ۲ کاراکتر باشد." }),
  email: z.email({ error: "ایمیل معتبر نیست." }),
  message: z
    .string()
    .trim()
    .min(10, { error: "پیام حداقل باید ۱۰ کاراکتر باشد." })
    .max(1000, { error: "پیام بیش از حد طولانی است." }),
});

type ContactErrors = Partial<
  Record<"name" | "email" | "message", string[]>
>;

export default function ContactForm() {
  const [errors, setErrors] = useState<ContactErrors>({});
  const [success, setSuccess] = useState(false);

  function handleSubmit(event: FormEvent<HTMLFormElement>) {
    event.preventDefault();

    const formData = new FormData(event.currentTarget);

    const input = {
      name: formData.get("name"),
      email: formData.get("email"),
      message: formData.get("message"),
    };

    const result = ContactSchema.safeParse(input);

    if (!result.success) {
      const flattened = z.flattenError(result.error);

      setErrors(flattened.fieldErrors);
      setSuccess(false);
      return;
    }

    setErrors({});
    setSuccess(true);

    console.log(result.data);
  }

  return (
    <form onSubmit={handleSubmit}>
      <label>
        نام
        <input name="name" />
      </label>
      {errors.name?.map((error) => (
        <p key={error}>{error}</p>
      ))}

      <label>
        ایمیل
        <input name="email" type="email" />
      </label>
      {errors.email?.map((error) => (
        <p key={error}>{error}</p>
      ))}

      <label>
        پیام
        <textarea name="message" />
      </label>
      {errors.message?.map((error) => (
        <p key={error}>{error}</p>
      ))}

      <button type="submit">ارسال</button>

      {success && <p>اطلاعات فرم معتبر است.</p>}
    </form>
  );
}

نکته مهم این است که اعتبارسنجی Frontend فقط برای بهبود تجربه کاربری است. همان داده باید در Backend نیز اعتبارسنجی شود؛ زیرا کاربر می‌تواند درخواست را بدون استفاده از فرم ارسال کند.

اعتبارسنجی req.body در Express

ابتدا وابستگی‌ها را نصب کنید:

npm install express zod
npm install --save-dev typescript tsx @types/express

Schema ساخت کاربر:

import { z } from "zod";

export const CreateUserSchema = z.object({
  name: z
    .string()
    .trim()
    .min(2)
    .max(100),
  email: z.email(),
  age: z.number().int().min(18).max(120),
});

export type CreateUserInput = z.infer<
  typeof CreateUserSchema
>;

Route مربوط به Express:

import express from "express";
import { z } from "zod";
import { CreateUserSchema } from "./schemas/user.js";

const app = express();

app.use(express.json());

app.post("/api/users", (req, res) => {
  const result = CreateUserSchema.safeParse(req.body);

  if (!result.success) {
    const errors = z.flattenError(result.error);

    return res.status(400).json({
      error: "VALIDATION_ERROR",
      message: "اطلاعات ارسال‌شده معتبر نیست.",
      fields: errors.fieldErrors,
    });
  }

  const user = result.data;

  return res.status(201).json({
    id: crypto.randomUUID(),
    ...user,
  });
});

app.listen(3000, () => {
  console.log("Server running on port 3000");
});

پس از safeParse، مقدار result.data هم اعتبارسنجی شده و هم دارای Type صحیح است. بهتر است از req.body خام در منطق برنامه استفاده نکنید.

اعتبارسنجی params و query در Express

Schema پارامتر مسیر:

const UserParamsSchema = z.object({
  userId: z.string().uuid(),
});

استفاده:

app.get("/api/users/:userId", (req, res) => {
  const result = UserParamsSchema.safeParse(req.params);

  if (!result.success) {
    return res.status(400).json({
      error: "INVALID_USER_ID",
    });
  }

  const { userId } = result.data;

  return res.json({
    id: userId,
  });
});

Schema برای Query String:

const ListUsersQuerySchema = z.object({
  page: z.coerce.number().int().min(1).default(1),
  limit: z.coerce
    .number()
    .int()
    .min(1)
    .max(100)
    .default(20),
  search: z.string().trim().max(100).optional(),
});

استفاده:

app.get("/api/users", (req, res) => {
  const result = ListUsersQuerySchema.safeParse(
    req.query,
  );

  if (!result.success) {
    return res.status(400).json({
      error: "INVALID_QUERY",
      fields: z.flattenError(result.error).fieldErrors,
    });
  }

  const { page, limit, search } = result.data;

  return res.json({
    page,
    limit,
    search,
    items: [],
  });
});

Query Stringها معمولاً به شکل رشته دریافت می‌شوند؛ به همین دلیل در این مثال از z.coerce.number() استفاده شده است.

ساخت Middleware عمومی Zod برای Express

برای جلوگیری از تکرار می‌توان Middleware ساخت:

import type {
  Request,
  Response,
  NextFunction,
} from "express";
import type { ZodType } from "zod";
import { z } from "zod";

export function validateBody(schema: ZodType) {
  return (
    req: Request,
    res: Response,
    next: NextFunction,
  ) => {
    const result = schema.safeParse(req.body);

    if (!result.success) {
      return res.status(400).json({
        error: "VALIDATION_ERROR",
        message: "بدنه درخواست معتبر نیست.",
        fields: z.flattenError(result.error).fieldErrors,
      });
    }

    req.body = result.data;
    next();
  };
}

استفاده در Route:

app.post(
  "/api/users",
  validateBody(CreateUserSchema),
  (req, res) => {
    return res.status(201).json({
      id: crypto.randomUUID(),
      ...req.body,
    });
  },
);

در پروژه‌های بزرگ‌تر بهتر است Type مربوط به req.body نیز در سطح Route مشخص شود یا داده اعتبارسنجی‌شده در یک Property مجزا قرار گیرد تا وابستگی به Mutation کاهش یابد.

اعتبارسنجی متغیرهای محیطی

اشتباه در متغیرهای محیطی معمولاً زمانی مشخص می‌شود که برنامه به بخش مربوط برسد. با Zod می‌توان هنگام شروع برنامه تمام تنظیمات ضروری را بررسی کرد.

import { z } from "zod";

const EnvSchema = z.object({
  NODE_ENV: z
    .enum(["development", "test", "production"])
    .default("development"),
  PORT: z.coerce.number().int().positive().default(3000),
  DARVAREH_API_KEY: z.string().min(1),
  DARVAREH_MODEL_ID: z.string().min(1),
});

const result = EnvSchema.safeParse(process.env);

if (!result.success) {
  console.error(
    z.prettifyError(result.error),
  );

  process.exit(1);
}

export const env = result.data;

فایل .env.example:

NODE_ENV=development
PORT=3000
DARVAREH_API_KEY=YOUR_DARVAREH_API_KEY
DARVAREH_MODEL_ID=YOUR_MODEL_ID

فایل .env واقعی نباید وارد Git شود:

.env
.env.*
!.env.example

اعتبارسنجی متغیر محیطی تضمین نمی‌کند کلید API معتبر یا دارای اعتبار مالی است؛ فقط وجود و شکل تنظیمات را بررسی می‌کند.

اعتبارسنجی پاسخ API با Zod

بسیاری از برنامه‌ها فقط Request را اعتبارسنجی می‌کنند، درحالی‌که Response خارجی نیز ممکن است تغییر کند یا ناقص باشد.

import { z } from "zod";

const PostSchema = z.object({
  id: z.number().int(),
  title: z.string(),
  body: z.string(),
});

const PostsSchema = z.array(PostSchema);

async function getPosts() {
  const response = await fetch(
    "https://example.com/api/posts",
  );

  if (!response.ok) {
    throw new Error(
      `Request failed: ${response.status}`,
    );
  }

  const rawData: unknown = await response.json();
  const result = PostsSchema.safeParse(rawData);

  if (!result.success) {
    throw new Error("API response schema is invalid");
  }

  return result.data;
}

اصل مهم این است که نتیجه response.json() را در مرز ورود داده به‌عنوان unknown در نظر بگیریم و پس از اعتبارسنجی از آن استفاده کنیم.

اعتبارسنجی پاسخ API هوش مصنوعی

پاسخ مدل‌های هوش مصنوعی نیز یک ورودی خارجی است. حتی اگر در پرامپت درخواست JSON کرده باشید، نباید صرفاً به ظاهر پاسخ اعتماد کنید.

Schema ساده برای پاسخ Chat Completions:

import { z } from "zod";

const ChatCompletionSchema = z.object({
  id: z.string().optional(),
  choices: z
    .array(
      z.object({
        message: z.object({
          role: z.string(),
          content: z.string().nullable(),
        }),
        finish_reason: z.string().nullable().optional(),
      }),
    )
    .min(1),
});

تابع ارسال درخواست به درواره:

export async function createChatCompletion(
  prompt: string,
) {
  const response = await fetch(
    "https://api.darvareh.ir/v1/chat/completions",
    {
      method: "POST",
      headers: {
        Authorization:
          `Bearer ${process.env.DARVAREH_API_KEY}`,
        "Content-Type": "application/json",
      },
      body: JSON.stringify({
        model: process.env.DARVAREH_MODEL_ID,
        messages: [
          {
            role: "user",
            content: prompt,
          },
        ],
      }),
    },
  );

  if (!response.ok) {
    throw new Error(
      `AI request failed: ${response.status}`,
    );
  }

  const rawData: unknown = await response.json();
  const result =
    ChatCompletionSchema.safeParse(rawData);

  if (!result.success) {
    throw new Error(
      "Unexpected AI API response structure",
    );
  }

  return result.data.choices[0].message.content;
}

مقادیر محیطی:

DARVAREH_API_KEY=YOUR_DARVAREH_API_KEY
DARVAREH_MODEL_ID=YOUR_MODEL_ID

کلید API باید فقط در Backend نگهداری شود و نباید در React، کد مرورگر، مخزن عمومی یا اپلیکیشن قابل استخراج قرار بگیرد.

برای دریافت API Key و استفاده از زیرساخت هوش مصنوعی می‌توانید در درواره ثبت‌نام کنید. شناسه مدل‌ها، قابلیت‌ها و قیمت‌های به‌روز نیز در صفحه مدل‌های درواره قرار دارد.

اعتبارسنجی JSON تولیدشده توسط مدل هوش مصنوعی

فرض کنید از مدل می‌خواهیم اطلاعات یک مقاله را به شکل JSON تولید کند:

{
  "title": "راهنمای TypeScript",
  "summary": "خلاصه مقاله",
  "tags": ["TypeScript", "JavaScript"],
  "difficulty": "beginner"
}

Schema:

const GeneratedArticleSchema = z.object({
  title: z.string().min(5).max(150),
  summary: z.string().min(20).max(500),
  tags: z
    .array(z.string().min(1).max(50))
    .min(1)
    .max(10),
  difficulty: z.enum([
    "beginner",
    "intermediate",
    "advanced",
  ]),
});

type GeneratedArticle = z.infer<
  typeof GeneratedArticleSchema
>;

تابع Parse:

function parseGeneratedArticle(
  content: string,
): GeneratedArticle {
  let json: unknown;

  try {
    json = JSON.parse(content);
  } catch {
    throw new Error(
      "Model output is not valid JSON",
    );
  }

  const result =
    GeneratedArticleSchema.safeParse(json);

  if (!result.success) {
    throw new Error(
      z.prettifyError(result.error),
    );
  }

  return result.data;
}

این فرایند دو مرحله دارد:

  1. بررسی معتبر بودن Syntax فایل JSON
  2. بررسی هماهنگی ساختار و مقادیر با Schema

معتبر بودن JSON به‌تنهایی کافی نیست. مقدار زیر JSON معتبر است، اما با Schema هماهنگ نیست:

{
  "title": 10,
  "summary": null,
  "tags": "TypeScript",
  "difficulty": "very-easy"
}

تولید JSON Schema از Zod

Zod 4 از تبدیل داخلی Schemaهای قابل پشتیبانی به JSON Schema برخوردار است. JSON Schema در OpenAPI، مستندسازی API و تعریف Structured Output مدل‌های هوش مصنوعی کاربرد دارد.

import { z } from "zod";

const ArticleSchema = z.object({
  title: z.string().min(5),
  summary: z.string(),
  tags: z.array(z.string()),
});

const jsonSchema = z.toJSONSchema(
  ArticleSchema,
);

console.log(jsonSchema);

خروجی مفهومی:

{
  "type": "object",
  "properties": {
    "title": {
      "type": "string",
      "minLength": 5
    },
    "summary": {
      "type": "string"
    },
    "tags": {
      "type": "array",
      "items": {
        "type": "string"
      }
    }
  },
  "required": [
    "title",
    "summary",
    "tags"
  ],
  "additionalProperties": false
}

تمام قابلیت‌های Zod را نمی‌توان بدون تغییر به JSON Schema تبدیل کرد. برای مثال بعضی Transformها رفتار اجرایی دارند و معادل مستقیمی در JSON Schema ندارند. جزئیات و محدودیت‌ها در مستندات JSON Schema در Zod توضیح داده شده است.

اعتبارسنجی ورودی Tool Calling

در یک AI Agent، مدل ممکن است آرگومان‌های یک ابزار را تولید کند. این آرگومان‌ها پیش از اجرای ابزار باید اعتبارسنجی شوند.

Schema ابزار جست‌وجوی محصول:

const SearchProductsArgsSchema = z.object({
  query: z.string().trim().min(2).max(100),
  category: z
    .enum([
      "laptop",
      "mobile",
      "accessory",
    ])
    .optional(),
  minPrice: z.number().nonnegative().optional(),
  maxPrice: z.number().nonnegative().optional(),
  limit: z.number().int().min(1).max(20).default(10),
});

بررسی ارتباط قیمت‌ها:

const ValidatedSearchProductsArgsSchema =
  SearchProductsArgsSchema.refine(
    (data) => {
      if (
        data.minPrice === undefined ||
        data.maxPrice === undefined
      ) {
        return true;
      }

      return data.maxPrice >= data.minPrice;
    },
    {
      error:
        "حداکثر قیمت نباید کمتر از حداقل قیمت باشد.",
      path: ["maxPrice"],
    },
  );

پیش از اجرای ابزار:

function executeSearchTool(
  rawArguments: unknown,
) {
  const result =
    ValidatedSearchProductsArgsSchema.safeParse(
      rawArguments,
    );

  if (!result.success) {
    return {
      success: false,
      error: "INVALID_TOOL_ARGUMENTS",
      details: z.flattenError(
        result.error,
      ).fieldErrors,
    };
  }

  return searchProducts(result.data);
}

اعتبارسنجی Schema به‌تنهایی جایگزین کنترل مجوز، محدودیت دسترسی، Rate Limit یا بررسی اثر عملیات نیست. برای ابزارهای دارای اثر خارجی باید لایه‌های کنترلی جداگانه وجود داشته باشد.

جلوگیری از ورود فیلدهای اضافی

رفتار Object Schema باید آگاهانه انتخاب شود. اگر می‌خواهید وجود هر کلید ناشناخته باعث خطا شود، از strictObject استفاده کنید:

const CreateUserSchema = z.strictObject({
  name: z.string(),
  email: z.email(),
});

ورودی زیر رد می‌شود:

{
  "name": "Sara",
  "email": "sara@example.com",
  "role": "admin"
}

این رفتار برای Endpointهایی مفید است که نباید فیلدهای خارج از قرارداد را بپذیرند.

در مقابل، اگر نیاز دارید کلیدهای ناشناخته نیز نگهداری شوند، باید رفتار Object را متناسب با نیاز طراحی کنید. پذیرش بدون بررسی فیلدهای اضافی می‌تواند باعث ابهام در قرارداد API شود.

اعتبارسنجی Async

اگر قاعده اعتبارسنجی به عملیات Async وابسته باشد، باید از parseAsync یا safeParseAsync استفاده کنید.

const UsernameSchema = z
  .string()
  .min(3)
  .refine(
    async (username) => {
      return await isUsernameAvailable(username);
    },
    {
      error: "این نام کاربری قبلاً ثبت شده است.",
    },
  );

استفاده:

const result =
  await UsernameSchema.safeParseAsync(
    "amir",
  );

برای بررسی‌های ساده ساختاری از Validation هم‌زمان استفاده کنید. عملیات دیتابیس یا شبکه را فقط زمانی وارد Validation کنید که طراحی پروژه آن را توجیه کند؛ زیرا می‌تواند زمان پاسخ و پیچیدگی مدیریت خطا را افزایش دهد.

تست کردن Zod Schema با Vitest

Schema نیز مانند سایر بخش‌های منطق برنامه باید تست شود.

import {
  describe,
  expect,
  it,
} from "vitest";
import { CreateUserSchema } from "./user-schema";

describe("CreateUserSchema", () => {
  it("accepts valid user data", () => {
    const result =
      CreateUserSchema.safeParse({
        name: "Sara",
        email: "sara@example.com",
        age: 28,
      });

    expect(result.success).toBe(true);
  });

  it("rejects invalid email", () => {
    const result =
      CreateUserSchema.safeParse({
        name: "Sara",
        email: "invalid",
        age: 28,
      });

    expect(result.success).toBe(false);
  });

  it("rejects users younger than 18", () => {
    const result =
      CreateUserSchema.safeParse({
        name: "Sara",
        email: "sara@example.com",
        age: 16,
      });

    expect(result.success).toBe(false);
  });
});

فقط داده معتبر را تست نکنید. موارد مرزی نیز مهم‌اند:

  • رشته خالی
  • مقدار null
  • فیلد حذف‌شده
  • عدد منفی
  • عدد اعشاری به‌جای Integer
  • Array خالی
  • مقدار دقیقاً روی حداقل و حداکثر
  • کلید اضافی
  • رشته بسیار طولانی
  • Type اشتباه

الگوی پیشنهادی ساختار فایل‌ها

در یک پروژه متوسط می‌توان Schemaها را در پوشه‌ای مستقل نگهداری کرد:

src/
  schemas/
    common.schema.ts
    user.schema.ts
    article.schema.ts
    ai-response.schema.ts
    env.schema.ts
  routes/
    users.route.ts
    articles.route.ts
  services/
    ai.service.ts
  middleware/
    validate.middleware.ts
  app.ts

نمونه Schema مشترک Pagination:

import { z } from "zod";

export const PaginationSchema = z.object({
  page: z.coerce.number().int().min(1).default(1),
  limit: z.coerce
    .number()
    .int()
    .min(1)
    .max(100)
    .default(20),
});

استفاده مجدد:

const SearchArticlesSchema =
  PaginationSchema.extend({
    query: z.string().trim().max(100).optional(),
    status: z
      .enum([
        "draft",
        "published",
        "archived",
      ])
      .optional(),
  });

اشتباهات رایج هنگام استفاده از Zod

استفاده از as به‌جای Validation

این کد اعتبارسنجی انجام نمی‌دهد:

const user = data as User;

راه مناسب:

const user = UserSchema.parse(data);

تعریف جداگانه Type و Schema

این روش احتمال ناهماهنگی را افزایش می‌دهد:

type User = {
  name: string;
};

const UserSchema = z.object({
  name: z.string(),
});

در حالت معمول بهتر است Type را استخراج کنید:

type User = z.infer<typeof UserSchema>;

اعتماد به اعتبارسنجی Frontend

اعتبارسنجی React قابل دور زدن است. تمام ورودی‌های مهم باید در Backend نیز بررسی شوند.

استفاده بیش از حد از coerce

Coercion می‌تواند مفید باشد، اما باید بدانید چه تبدیل‌هایی انجام می‌شود. اگر ورودی باید واقعاً Number باشد، از z.number() استفاده کنید.

نمایش مستقیم تمام خطاها به کاربر

خطاهای داخلی ممکن است برای توسعه‌دهنده مناسب اما برای کاربر نامفهوم باشند. پیام عمومی کاربر و جزئیات فنی لاگ باید از یکدیگر جدا شوند.

اعتبارسنجی دیرهنگام

داده خارجی را بلافاصله پس از ورود به سیستم اعتبارسنجی کنید. هرچه داده خام بیشتر در برنامه گردش کند، پیدا کردن منشأ خطا دشوارتر می‌شود.

Schemaهای بسیار بزرگ

یک Schema چندصدخطی نگهداری دشواری دارد. ساختار را به Schemaهای کوچک‌تر و قابل استفاده مجدد تقسیم کنید.

تصور اینکه Validation همان Sanitization است

Validation بررسی می‌کند داده با قرارداد سازگار است. Sanitization، مجوز دسترسی، Escape کردن خروجی، کنترل Query دیتابیس و سایر لایه‌های ایمنی وظایف جداگانه‌ای هستند.

اجرای مجدد Validation بدون نیاز

اگر داده در مرز ورودی اعتبارسنجی و سپس در لایه‌های داخلی به‌صورت Type-Safe منتقل شده است، اجرای مکرر همان Schema در تمام توابع ممکن است هزینه و پیچیدگی غیرضروری ایجاد کند.

بهینه‌سازی عملکرد Zod

برای بیشتر فرم‌ها و APIهای معمولی، عملکرد Zod مناسب است؛ اما در پردازش حجم بسیار زیاد داده باید اندازه‌گیری واقعی انجام شود.

چند توصیه عملی:

  • Schemaها را یک بار در سطح Module تعریف کنید.
  • داخل هر Request Schema یکسان را دوباره نسازید.
  • داده را در مرز ورودی اعتبارسنجی کنید.
  • برای فایل‌ها یا Arrayهای بسیار بزرگ، پردازش Chunkشده را بررسی کنید.
  • از Refinementهای Async غیرضروری خودداری کنید.
  • پیش از بهینه‌سازی، Benchmark متناسب با داده واقعی پروژه بگیرید.
  • خطاهای کامل را برای تمام Requestهای نامعتبر در Logهای عمومی ذخیره نکنید.
  • برای Frontendهای بسیار حساس به حجم Bundle، Zod Mini را فقط پس از اندازه‌گیری بررسی کنید.

مستندات رسمی، Zod Mini را نسخه‌ای Tree-Shakable با API تابعی معرفی می‌کند؛ بااین‌حال برای بیشتر پروژه‌ها استفاده از Zod معمولی ساده‌تر و خواناتر است. جزئیات در راهنمای Zod Mini قرار دارد.

چک‌لیست استفاده از Zod در پروژه واقعی

  • TypeScript با strict فعال شده است.
  • داده خارجی ابتدا به‌عنوان unknown در نظر گرفته می‌شود.
  • Schemaها در مرز ورودی سیستم اجرا می‌شوند.
  • Typeها با z.infer از Schema استخراج می‌شوند.
  • فرم Frontend و Endpoint Backend هر دو اعتبارسنجی دارند.
  • safeParse برای خطاهای قابل انتظار استفاده می‌شود.
  • پیام خطای کاربر از جزئیات فنی جدا شده است.
  • req.body، req.params و req.query بررسی می‌شوند.
  • پاسخ APIهای خارجی اعتبارسنجی می‌شود.
  • خروجی JSON مدل هوش مصنوعی پیش از استفاده Parse و Validate می‌شود.
  • آرگومان Tool Calling پیش از اجرای ابزار بررسی می‌شود.
  • متغیرهای محیطی هنگام شروع برنامه اعتبارسنجی می‌شوند.
  • Schemaها برای ورودی‌های معتبر، نامعتبر و مرزی تست دارند.
  • کلید API فقط در Backend و متغیر محیطی نگهداری می‌شود.
  • Validation جایگزین مجوز دسترسی، تست و بازبینی انسانی در نظر گرفته نشده است.

پرسش‌های متداول

آیا Zod فقط برای TypeScript است؟

Zod با JavaScript نیز کار می‌کند، اما بیشترین مزیت آن در TypeScript و قابلیت استخراج Type خودکار دیده می‌شود.

آیا Zod جایگزین TypeScript است؟

خیر. TypeScript کد را در Compile Time بررسی می‌کند و Zod داده واقعی را در Runtime اعتبارسنجی می‌کند.

تفاوت parse و safeParse چیست؟

parse در صورت نامعتبر بودن داده خطا ایجاد می‌کند. safeParse خطا پرتاب نمی‌کند و یک Object شامل success و سپس data یا error برمی‌گرداند.

آیا Zod برای اعتبارسنجی فرم React مناسب است؟

بله. می‌توان Zod را به‌صورت مستقیم یا همراه کتابخانه‌های مدیریت فرم استفاده کرد. اعتبارسنجی Backend همچنان ضروری است.

آیا Zod داده را نیز تبدیل می‌کند؟

بله. امکاناتی مانند transform، coerce، default، pipe و در نسخه‌های جدید Codecs برای تبدیل کنترل‌شده داده وجود دارند.

آیا Zod پاسخ API را بررسی می‌کند؟

اگر Schema پاسخ را تعریف و داده را با parse یا safeParse بررسی کنید، بله. اضافه کردن Type به متغیر بدون Parse کردن داده، اعتبارسنجی Runtime محسوب نمی‌شود.

آیا می‌توان Zod Schema را به JSON Schema تبدیل کرد؟

در Zod 4 می‌توان بسیاری از Schemaها را با z.toJSONSchema() تبدیل کرد. برخی رفتارهای اجرایی مانند Transformها ممکن است معادل مستقیم JSON Schema نداشته باشند.

آیا Zod برای خروجی هوش مصنوعی مناسب است؟

بله. Zod برای اعتبارسنجی JSON، Structured Output و آرگومان Tool Calling مفید است. بااین‌حال باید حالت‌های خطا، پاسخ ناقص و تلاش مجدد نیز در طراحی برنامه در نظر گرفته شوند.

آیا Zod از اعتبارسنجی Async پشتیبانی می‌کند؟

بله. برای Schemaهای دارای Refinement یا Transform ناهمگام باید از parseAsync یا safeParseAsync استفاده کنید.

آیا Validation با Zod امنیت API را تضمین می‌کند؟

خیر. Zod فقط یکی از لایه‌های کنترل ورودی است. احراز هویت، مجوز دسترسی، Rate Limit، مدیریت Secret، Queryهای پارامتری، کنترل عملیات و ثبت رویدادها باید جداگانه پیاده‌سازی شوند.

جمع‌بندی

Zod فاصله میان Typeهای TypeScript و داده واقعی Runtime را پر می‌کند. با این کتابخانه می‌توان یک Schema واحد تعریف کرد، داده‌های ورودی را اعتبارسنجی کرد و Typeهای TypeScript را نیز از همان Schema استخراج کرد.

مهم‌ترین محل استفاده Zod مرزهای سیستم است؛ یعنی جایی که داده از فرم، API، Query String، متغیر محیطی، فایل JSON یا مدل هوش مصنوعی وارد برنامه می‌شود. اعتبارسنجی سریع در همین نقطه باعث می‌شود خطاها زودتر، واضح‌تر و نزدیک‌تر به منشأ خود شناسایی شوند.

برای شروع کافی است Zod را نصب کنید، یک Schema کوچک بسازید، Type را با z.infer استخراج کنید و ورودی را با safeParse بررسی کنید. پس از آن می‌توانید Schemaها را در فرم React، Routeهای Express، پاسخ APIها، Structured Output و ابزارهای AI Agent گسترش دهید.

در پروژه‌های مبتنی بر هوش مصنوعی نیز خروجی مدل را یک داده خارجی در نظر بگیرید. حتی اگر مدل موظف به تولید JSON باشد، نتیجه را Parse و با Schema اعتبارسنجی کنید و فقط پس از موفقیت وارد منطق برنامه کنید.

منابع تکمیلی

مقالات مرتبط

برای مطالعه شرایط استفاده و محدودیت‌های مسئولیت، صفحه «سلب مسئولیت» را مشاهده کنید.

Read more

اتوماسیون هوش مصنوعی چیست؟ کاربردها و آموزش ساخت AI Automation

اتوماسیون هوش مصنوعی چیست؟ کاربردها و آموزش ساخت AI Automation

اتوماسیون هوش مصنوعی با ترکیب گردش‌کارهای خودکار و مدل‌های هوش مصنوعی، پردازش متن، دسته‌بندی، استخراج اطلاعات و تصمیم‌های پیشنهادی را خودکار می‌کند. در این راهنما، معماری و ساخت نمونه عملی آن با API درواره را می‌آموزید.

Agentic Commerce چیست؟ آینده خرید با ایجنت هوش مصنوعی

Agentic Commerce چیست؟ آینده خرید با ایجنت هوش مصنوعی

Agentic Commerce شیوه‌ای جدید برای خرید اینترنتی است که در آن ایجنت هوش مصنوعی می‌تواند نیاز کاربر را بفهمد، محصولات را جست‌وجو و مقایسه کند و فرایند خرید را پیش ببرد. در این راهنما با معماری، UCP، ACP و پیاده‌سازی آن با API درواره آشنا می‌شوید.