Menu
CoddyTech

Schematy z zod

Część sekcji API REST ścieżki Node.js w Coddy. Lekcja 11 z 25.

Ręczne sprawdzanie rozrasta się wraz z każdym polem i każdą trasą. Schemat zod opisuje strukturę jeden raz, a safeParse sprawdza wartość pod jego kątem:

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 zawiera zweryfikowaną treść żądania, z usuniętymi białymi znakami na początku i końcu name

.strict() odrzuca pola, których nie ma na liście schematu. Event.partial() sprawia, że wszystkie pola są opcjonalne — tego wymaga żądanie PATCH. Event.partial({ seats: true }) sprawia, że opcjonalne jest tylko seats, a const { seats = 1 } = result.data używa wartości 1, jeśli jej brakuje. z.flattenError(result.error) zamienia problemy na { formErrors, fieldErrors }: formErrors zawiera problemy dotyczące całego obiektu body, takie jak nieznane pole, a fieldErrors zawiera problemy dotyczące poszczególnych pól. Event.parse wykonuje tę samą kontrolę, ale zgłasza wyjątek w przypadku nieprawidłowej wartości, a wyjątek w trasie kończy się błędem 500.

challenge icon

Wyzwanie

Łatwy

Ten krok otwiera przygotowany w kursie projekt API do śledzenia zadań, a nie pliki projektu z lekcji Validating Input (w tym routes/tasks.js). Twoja wcześniejsza praca pozostaje w osobnej lekcji. Przeczytaj dostarczone pliki przed edytowaniem routes/tasks.js. Ta kopia zawiera trasy CRUD zadań i ręcznie napisane sprawdzanie danych wejściowych.

Ręcznie napisanych kontroli przybywa wraz z każdym polem i każdą trasą. Opisz zadanie raz za pomocą schematu zod i użyj go we wszystkich trzech trasach przyjmujących treść żądania.

  1. W pliku routes/tasks.js zaimportuj z z zod i napisz schemat o nazwie Task:
    • title: ciąg znaków, przycięty do 1–100 znaków po usunięciu białych znaków z początku i końca.
    • done: true lub false (z.boolean()).
    • Schemat jest ścisły, więc pole, którego nie wymienia, na przykład color, zostaje odrzucone.

    Usuń dostarczoną funkcję pomocniczą titleError (wersję sprawdzania danych z lekcji Validating Input przygotowaną w kursie).

  2. Utwórz dwie wersje Task za pomocą .partial(). Wersja dla POST sprawia, że opcjonalne jest tylko done. Wersja dla PATCH sprawia, że opcjonalne są oba pola. PUT używa samego Task, więc wymaga obu pól.
  3. W POST, PATCH i PUT sprawdzaj req.body za pomocą safeParse. Jeśli sprawdzenie się nie powiedzie, zwróć status 400 i umieść listę problemów zod pod kluczem error, utworzoną za pomocą z.flattenError(result.error). Dla { "title": 123 } wygląda to tak:
    { "error": { "formErrors": [], "fieldErrors": { "title": ["Invalid input: expected string, received number"] } } }
  4. Jeśli sprawdzenie się powiedzie, zapisuj dane z result.data, a nie z req.body: zawierają one przycięty tytuł. W POST treść żądania bez done tworzy zadanie z "done": false (brakujące pole ma wartość undefined w result.data, a ?? zapewnia wartość domyślną). PUT nadal zachowuje id i createdAt z zapisanego zadania.

Wypróbuj: Wyślij POST z treścią { "title": " Order new chairs ", "color": "red" } do /api/tasks. Otrzymasz 400, a formErrors będzie zawierać informację Unrecognized key: "color". Wyślij ponownie bez color: otrzymasz 201 z "title": "Order new chairs" (bez spacji) i "done": false. Żądanie PATCH /api/tasks/2 z treścią { "done": "yes" } zwróci 400 z wpisem done w fieldErrors. Żądanie PUT /api/tasks/2 z treścią { "title": "Book the big room", "done": false } zwróci 200, zachowując stare createdAt zadania 2.

Spróbuj swoich sił

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

const router = Router();

// Powód, dla którego title nie może zostać zapisany, lub null, jeśli może.
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;

Wszystkie lekcje w sekcji API REST

Poćwicz samodzielnie: Edytor online Express