Menu
CoddyTech

المخططات باستخدام zod

جزء من قسم واجهات REST API في رحلة Node.js على Coddy. الدرس 7 من 18.

تزداد الفحوصات اليدوية مع كل حقل وكل مسار. يصف مخطط 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 وحده اختياريًا. تسرد z.flattenError المشكلات حسب الحقل. يجري Event.parse الفحص نفسه، لكنه يطلق استثناءً عند وجود قيمة غير صالحة، والاستثناء في المسار يتحول إلى 500.

challenge icon

التحدي

سهل
  • استبدل عمليات التحقق اليدوية بمخطط Task: title سلسلة نصية بعد إزالة المسافات، بطول يتراوح بين 1 و100 حرف، وdone قيمة منطقية، و.strict().
  • تستخدم POST (حيث تكون done اختيارية، وقيمتها الافتراضية false) وPATCH (كل الحقول اختيارية) وPUT الدالة safeParse؛ ويؤدي وجود جسم طلب غير صالح إلى استجابة 400 تتضمن z.flattenError(result.error) في الحقل error.

أرسل طلب POST بهذا المحتوى: { "title": " Order new chairs ", "color": "red" }، ثم أرسله دون color.

جرّب بنفسك

import { Router } from 'express';
import { nextId, tasks } from '../store.js';

const router = Router();

// سبب تعذّر تخزين title، أو 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