סכמות עם 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.
אתגר
קלבשלב הזה נפתח פרויקט מעקב המשימות של ה-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 עם { "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 אונליין