Schemas mit zod
Teil des Abschnitts REST-APIs der Node.js-Journey von Coddy. Lektion 11 von 25.
Manuelle Prüfungen nehmen mit jedem Feld und jeder Route zu. Ein zod-Schema beschreibt die Struktur einmal, und safeParse prüft einen Wert dagegen:
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 enthält den validierten Body, wobei name getrimmt ist.strict() weist Felder zurück, die das Schema nicht aufführt. Event.partial() macht alle Felder optional, was für einen PATCH erforderlich ist. Event.partial({ seats: true }) macht nur seats optional, und const { seats = 1 } = result.data verwendet 1, wenn der Wert fehlt. z.flattenError(result.error) wandelt die Probleme in { formErrors, fieldErrors } um: formErrors enthält Probleme mit dem gesamten Body, etwa ein unbekanntes Feld, und fieldErrors enthält die Probleme der einzelnen Felder. Event.parse führt dieselbe Prüfung durch, löst bei einem ungültigen Wert jedoch einen Fehler aus, und ein Fehler in einer Route führt zu einem 500-Statuscode.
Aufgabe
EinfachIn diesem Schritt wird das vorbereitete Task-Tracker-API-Projekt des Kurses geöffnet und nicht deine Projektdateien aus Validating Input (einschließlich routes/tasks.js). Deine frühere Arbeit bleibt in ihrer eigenen Lektion. Lies die bereitgestellten Dateien, bevor du routes/tasks.js bearbeitest. Diese Kopie enthält CRUD-Routen für Aufgaben und eine handgeschriebene Eingabevalidierung.
Deine handgeschriebenen Prüfungen werden mit jedem Feld und jeder Route umfangreicher. Beschreibe eine Aufgabe einmal mit einem zod-Schema und lass alle drei Routen, die einen Body entgegennehmen, dieses verwenden.
- Importiere in
routes/tasks.jszauszodund schreibe ein Schema namensTask:title: eine Zeichenfolge, die getrimmt wird und nach dem Trimmen 1 bis 100 Zeichen umfasst.done:trueoderfalse(z.boolean()).- Das Schema ist strikt, daher wird ein darin nicht aufgeführtes Feld wie
colorabgewiesen.
Lösche den bereitgestellten Helfer
titleError(die Kursversion der Validierung aus Validating Input). - Erstelle mit
.partial()zwei Varianten vonTask. Bei der POST-Variante ist nurdoneoptional. Bei der PATCH-Variante sind beide Felder optional. PUT verwendetTaskselbst und benötigt daher beide Felder. - Prüfe in POST, PATCH und PUT
req.bodymitsafeParse. Schlägt die Prüfung fehl, antworte mit Status 400 und zods Liste der Probleme untererror, erstellt mitz.flattenError(result.error). Für{ "title": 123 }sieht das so aus:{ "error": { "formErrors": [], "fieldErrors": { "title": ["Invalid input: expected string, received number"] } } } - Wenn die Prüfung erfolgreich ist, speichere die Werte aus
result.dataund nicht ausreq.body: Dort ist der getrimmte Titel enthalten. In POST erstellt ein Body ohnedonedie Aufgabe mit"done": false(ein fehlendes Feld ist inresult.dataundefined, und??liefert einen Standardwert). PUT behält weiterhinidundcreatedAtaus der gespeicherten Aufgabe bei.
Probiere es aus: Sende per POST { "title": " Order new chairs ", "color": "red" } an /api/tasks. Du erhältst 400, und formErrors enthält Unrecognized key: "color". Sende es erneut ohne color: Du erhältst 201 mit "title": "Order new chairs" (ohne Leerzeichen) und "done": false. PATCH /api/tasks/2 mit { "done": "yes" } gibt 400 mit einem Eintrag für done in fieldErrors zurück. PUT /api/tasks/2 mit { "title": "Book the big room", "done": false } gibt 200 zurück und behält das alte createdAt von Aufgabe 2 bei.
Probier es selbst
import { Router } from 'express';
import { nextId, tasks } from '../store.js';
const router = Router();
// Warum title nicht gespeichert werden kann, oder null, wenn es möglich ist.
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;
Alle Lektionen in REST-APIs
1Ressourcen und Verben
Ressourcen und URLsErstellen mit POST4Browser, Versionen und Konfiguration
Was ist ein Origin?Einen anderen Origin zulassenCORSEine Seite, die die API aufruft2Validierung und Fehler
Eingaben validierenÜbe selbstständig: Express-Playground