Menu
CoddyTech

Schemas mit zod

Teil des Abschnitts REST-APIs der Node.js-Journey von Coddy. Lektion 7 von 18.

Manuelle Prüfungen nehmen mit jedem Feld und jeder Route zu. Ein zod-Schema beschreibt die Form 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 im Schema nicht aufgeführt sind. Event.partial() macht jedes Feld optional, was für einen PATCH erforderlich ist, und Event.partial({ seats: true }) macht nur seats optional. z.flattenError listet die Probleme nach Feld auf. Event.parse führt dieselbe Prüfung durch, löst bei einem ungültigen Wert aber einen Fehler aus, und ein Fehler in einer Route führt zu einem 500-Status.

challenge icon

Aufgabe

Einfach
  • Ersetze die manuellen Prüfungen durch ein Task-Schema: title ist eine getrimmte Zeichenkette mit 1 bis 100 Zeichen, done ist ein boolescher Wert, .strict().
  • POST (done optional, Standardwert false), PATCH (alle Felder optional) und PUT verwenden safeParse; bei einem ungültigen Body wird mit Status 400 z.flattenError(result.error) als error zurückgegeben.

Sende eine POST-Anfrage mit { "title": " Order new chairs ", "color": "red" } und danach ohne color.

Probier es selbst

import { Router } from 'express';
import { nextId, tasks } from '../store.js';

const router = Router();

// Warum ein title nicht gespeichert werden kann, oder null, wenn er gespeichert werden kann.
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