المخططات باستخدام zod
جزء من قسم واجهات REST API في رحلة Node.js على Coddy. الدرس 11 من 25.
تزداد الفحوصات اليدوية مع كل حقل وكل مسار. يصف مخطط zod البنية مرة واحدة، وتختبر safeParse قيمةً بمقارنتها به:
import { z } from 'zod';
const Event = z.object({
name: z.string().trim().min(1).max(80),
seats: z.number().int().min(1),
}).strict();
const result = Event.safeParse(req.body);
if (!result.success) {
return res.status(400).json({ error: z.flattenError(result.error) });
}
// يحتوي result.data على body بعد التحقق منه، مع إزالة المسافات الزائدة من nameيرفض .strict() الحقول التي لا يدرجها المخطط. يجعل Event.partial() كل حقل اختياريًا، وهذا ما يحتاج إليه طلب PATCH. يجعل Event.partial({ seats: true }) الحقل seats وحده اختياريًا، ويستخدم const { seats = 1 } = result.data القيمة 1 إذا كان الحقل مفقودًا. يحوّل z.flattenError(result.error) المشكلات إلى { formErrors, fieldErrors }: يحتوي formErrors على المشكلات المتعلقة بالجسم بأكمله، مثل وجود حقل غير معروف، ويحتوي fieldErrors على مشكلات كل حقل. يُجري Event.parse الفحص نفسه، لكنه يطلق استثناءً عند وجود قيمة غير صالحة، والاستثناء في المسار يؤدي إلى استجابة 500.
التحدي
سهلتفتح هذه الخطوة مشروع واجهة API لتعقّب المهام الذي أعدّه المقرر، بدلًا من ملفات مشروعك من درس التحقق من صحة المدخلات (بما في ذلك routes/tasks.js). يظل عملك السابق ضمن درسه الخاص. اقرأ الملفات المتوفرة قبل تعديل routes/tasks.js. تتضمن هذه النسخة مسارات CRUD للمهام والتحقق اليدوي من صحة المدخلات.
تزداد فحوصاتك اليدوية مع كل حقل وكل مسار. صِف المهمة مرة واحدة باستخدام مخطط zod، ودع المسارات الثلاثة التي تستقبل جسم الطلب تستخدمه.
- في
routes/tasks.js، استوردzمنzodواكتب مخططًا باسمTask:title: سلسلة نصية، تُزال المسافات الزائدة من طرفيها، ويتراوح طولها بين 1 و100 محرف بعد إزالة المسافات.done: إماtrueأوfalse(z.boolean()).- المخطط صارم، لذا يُرفض أي حقل غير مُدرج فيه، مثل
color.
احذف الدالة المساعدة
titleErrorالمتوفرة (نسخة المقرر من التحقق من الصحة في درس التحقق من صحة المدخلات). - أنشئ نسختين من
Taskباستخدام.partial(). تجعل نسخة POST الحقلdoneوحده اختياريًا. أما نسخة PATCH فتجعل كلا الحقلين اختياريين. تستخدم PUT المخططTaskنفسه، لذا يتطلب كلا الحقلين. - في POST وPATCH وPUT، تحقّق من
req.bodyباستخدامsafeParse. عند فشل التحقق، أعد استجابة بالحالة 400، وضع قائمة المشكلات من zod تحتerror، باستخدامz.flattenError(result.error). بالنسبة إلى{ "title": 123 }، ستبدو النتيجة هكذا:{ "error": { "formErrors": [], "fieldErrors": { "title": ["Invalid input: expected string, received number"] } } } - عند نجاح التحقق، احفظ البيانات من
result.data، وليس منreq.body: فهي تحتوي على العنوان بعد إزالة المسافات الزائدة. في POST، يؤدي إرسال جسم طلب بلاdoneإلى إنشاء المهمة مع"done": false(يكون الحقل المفقودundefinedفيresult.data، وتوفر??قيمة بديلة). ويظل PUT محتفظًا بقيمتيidوcreatedAtمن المهمة المخزنة.
جرّب ذلك: أرسل POST إلى /api/tasks مع { "title": " Order new chairs ", "color": "red" }. ستحصل على 400، وستذكر formErrors الرسالة Unrecognized key: "color". أعد الإرسال من دون color: ستحصل على 201 مع "title": "Order new chairs" (من دون مسافات) و"done": false. يؤدي إرسال { "done": "yes" } إلى PATCH /api/tasks/2 إلى إرجاع 400 مع إدخال done في fieldErrors. ويؤدي إرسال { "title": "Book the big room", "done": false } إلى PUT /api/tasks/2 إلى إرجاع 200، مع قيمة createdAt القديمة للمهمة 2.
جرّب بنفسك
import { Router } from 'express';
import { nextId, tasks } from '../store.js';
const router = Router();
// سبب تعذّر تخزين العنوان، أو null إذا أمكن تخزينه.
const titleError = (title) => {
if (typeof title !== 'string' || title.trim() === '' || title.length > 100) {
return 'title must be a string of 1 to 100 characters';
}
return null;
};
router.get('/', (req, res) => {
res.json(tasks);
});
router.get('/:id', (req, res) => {
const task = tasks.find((t) => t.id === Number(req.params.id));
if (!task) {
return res.status(404).json({ error: 'Task not found' });
}
res.json(task);
});
router.post('/', (req, res) => {
const { title } = req.body ?? {};
const problem = titleError(title);
if (problem) {
return res.status(400).json({ error: problem });
}
const task = { id: nextId(), title, done: false, createdAt: new Date().toISOString() };
tasks.push(task);
res.status(201).location(`/api/tasks/${task.id}`).json(task);
});
router.patch('/:id', (req, res) => {
const task = tasks.find((t) => t.id === Number(req.params.id));
if (!task) {
return res.status(404).json({ error: 'Task not found' });
}
const { title, done } = req.body ?? {};
if (title !== undefined && titleError(title)) {
return res.status(400).json({ error: titleError(title) });
}
if (done !== undefined && typeof done !== 'boolean') {
return res.status(400).json({ error: 'done must be true or false' });
}
if (title !== undefined) task.title = title;
if (done !== undefined) task.done = done;
res.json(task);
});
router.put('/:id', (req, res) => {
const index = tasks.findIndex((t) => t.id === Number(req.params.id));
if (index === -1) {
return res.status(404).json({ error: 'Task not found' });
}
const { title, done } = req.body ?? {};
if (title === undefined || done === undefined) {
return res.status(400).json({ error: 'PUT needs every field: title and done' });
}
const { id, createdAt } = tasks[index];
tasks[index] = { id, title, done, createdAt };
res.json(tasks[index]);
});
router.delete('/:id', (req, res) => {
const index = tasks.findIndex((t) => t.id === Number(req.params.id));
if (index === -1) {
return res.status(404).json({ error: 'Task not found' });
}
tasks.splice(index, 1);
res.status(204).end();
});
export default router;
جميع دروس واجهات REST API
1الموارد والأفعال
الموارد وعناوين URLالإنشاء باستخدام POST4المتصفحات والإصدارات والإعدادات
ما هو المصدرالسماح لمصدر آخرCORSصفحة تستدعي API2التحقق والأخطاء
التحقق من المدخلاتتدرّب بنفسك: Playground لـ Express