Menu
CoddyTech

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.

challenge icon

Aufgabe

Einfach

In 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.

  1. Importiere in routes/tasks.js z aus zod und schreibe ein Schema namens Task:
    • title: eine Zeichenfolge, die getrimmt wird und nach dem Trimmen 1 bis 100 Zeichen umfasst.
    • done: true oder false (z.boolean()).
    • Das Schema ist strikt, daher wird ein darin nicht aufgeführtes Feld wie color abgewiesen.

    Lösche den bereitgestellten Helfer titleError (die Kursversion der Validierung aus Validating Input).

  2. Erstelle mit .partial() zwei Varianten von Task. Bei der POST-Variante ist nur done optional. Bei der PATCH-Variante sind beide Felder optional. PUT verwendet Task selbst und benötigt daher beide Felder.
  3. Prüfe in POST, PATCH und PUT req.body mit safeParse. Schlägt die Prüfung fehl, antworte mit Status 400 und zods Liste der Probleme unter error, erstellt mit z.flattenError(result.error). Für { "title": 123 } sieht das so aus:
    { "error": { "formErrors": [], "fieldErrors": { "title": ["Invalid input: expected string, received number"] } } }
  4. Wenn die Prüfung erfolgreich ist, speichere die Werte aus result.data und nicht aus req.body: Dort ist der getrimmte Titel enthalten. In POST erstellt ein Body ohne done die Aufgabe mit "done": false (ein fehlendes Feld ist in result.data undefined, und ?? liefert einen Standardwert). PUT behält weiterhin id und createdAt aus 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

Übe selbstständig: Express-Playground