Menu
CoddyTech

סכמות עם zod

חלק מהיחידה REST APIs במסלול ה-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 מכיל את גוף הבקשה לאחר בדיקה, כאשר הרווחים בתחילת ובסוף 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();

// הסיבה לכך שלא ניתן לשמור כותרת, או 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 APIs