Menu
CoddyTech

סכמות עם zod

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

challenge icon

אתגר

קל

בשלב הזה נפתח פרויקט מעקב המשימות של ה-API שהוכן עבור הקורס, במקום קובצי הפרויקט שלך מתוך אימות קלט (כולל routes/tasks.js). העבודה הקודמת שלך נשארת בשיעור משלה. קרא את הקבצים שסופקו לפני עריכת routes/tasks.js. העותק הזה מכיל נתיבי CRUD למשימות ואימות קלט שנכתב ידנית.

ככל שיש יותר שדות ונתיבים, כך הבדיקות שכתבת ידנית מתרבות. תאר משימה פעם אחת באמצעות סכמת zod, והשתמש בה בכל שלושת הנתיבים שמקבלים גוף בקשה.

  1. ב-routes/tasks.js, ייבא את z מתוך zod וכתוב סכמה בשם Task:
    • title: מחרוזת שעוברת הסרת רווחים מההתחלה ומהסוף, באורך של 1 עד 100 תווים לאחר הסרת הרווחים.
    • done: true או false (z.boolean()).
    • הסכמה היא קפדנית, ולכן שדה שאינו מופיע בה, כמו color, נדחה.

    מחק את פונקציית העזר שסופקה, titleError (הגרסה של הקורס לאימות מתוך אימות קלט).

  2. צור שתי גרסאות של Task באמצעות .partial(). בגרסת POST רק done הוא אופציונלי. בגרסת PATCH שני השדות אופציונליים. PUT משתמשת ב-Task עצמה, ולכן היא דורשת את שני השדות.
  3. ב-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"] } } }
  4. אם הבדיקה מצליחה, שמור מתוך result.data, ולא מתוך req.body: הוא מכיל את הכותרת לאחר הסרת הרווחים. ב-POST, גוף בקשה ללא done יוצר את המשימה עם "done": false (שדה חסר הוא undefined בתוך result.data, ו-?? מספק ערך חלופי). PUT עדיין שומרת את id ואת createdAt מהמשימה המאוחסנת.

נסה זאת: שלח POST עם { "title": " Order new chairs ", "color": "red" } אל /api/tasks. תקבל 400, וב-formErrors יופיע Unrecognized key: "color". שלח שוב בלי color: תקבל 201 עם "title": "Order new chairs" (ללא רווחים) ועם "done": false. שלח PATCH /api/tasks/2 עם { "done": "yes" }; התגובה תהיה 400, ויופיע ערך עבור done בתוך fieldErrors. שלח PUT /api/tasks/2 עם { "title": "Book the big room", "done": false }; התגובה תהיה 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 APIs

תרגלו בעצמכם: Express אונליין