Menu
CoddyTech

Schemi con zod

Fa parte della sezione API REST del percorso Node.js di Coddy. Lezione 11 di 25.

I controlli manuali aumentano con ogni campo e ogni route. Uno schema zod descrive una volta sola la struttura, e safeParse verifica un valore confrontandolo con essa:

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 contiene il body verificato, con name senza spazi iniziali e finali

.strict() rifiuta i campi non elencati nello schema. Event.partial() rende facoltativo ogni campo, proprio ciò che serve a una PATCH. Event.partial({ seats: true }) rende facoltativo solo seats, e const { seats = 1 } = result.data usa 1 quando il campo è assente. z.flattenError(result.error) trasforma i problemi in { formErrors, fieldErrors }: formErrors contiene i problemi relativi all'intero corpo, come un campo sconosciuto, e fieldErrors contiene i problemi di ciascun campo. Event.parse esegue lo stesso controllo, ma genera un'eccezione in caso di valore non valido, e un'eccezione in una route diventa un 500.

challenge icon

Sfida

Facile

Questo passaggio apre il progetto API del tracker delle attività preparato per il corso, anziché i file del tuo progetto di Validating Input (incluso routes/tasks.js). Il tuo lavoro precedente rimane nella sua lezione. Leggi i file forniti prima di modificare routes/tasks.js. Questa copia contiene le route CRUD per le attività e la convalida dell’input scritta a mano.

I controlli scritti a mano aumentano con ogni campo e ogni route. Descrivi un’attività una sola volta con uno schema zod e usalo in tutte e tre le route che accettano un corpo.

  1. In routes/tasks.js, importa z da zod e scrivi uno schema chiamato Task:
    • title: una stringa, senza spazi iniziali o finali, da 1 a 100 caratteri dopo aver rimosso gli spazi.
    • done: true oppure false (z.boolean()).
    • Lo schema è rigoroso, quindi rifiuta i campi non elencati, come color.

    Elimina l’helper fornito titleError (la versione del corso della convalida di Validating Input).

  2. Crea due versioni di Task con .partial(). La versione di POST rende facoltativo solo done. La versione di PATCH rende facoltativi entrambi i campi. PUT usa direttamente Task, quindi ha bisogno di entrambi i campi.
  3. In POST, PATCH e PUT, controlla req.body con safeParse. Se il controllo non va a buon fine, rispondi con stato 400 e l’elenco dei problemi di zod sotto error, ottenuto con z.flattenError(result.error). Per { "title": 123 } il risultato è simile a questo:
    { "error": { "formErrors": [], "fieldErrors": { "title": ["Invalid input: expected string, received number"] } } }
  4. Se il controllo va a buon fine, salva i dati da result.data, non da req.body: contiene il titolo senza spazi iniziali o finali. In POST, un corpo senza done crea l’attività con "done": false (un campo mancante è undefined in result.data e ?? fornisce un valore di ripiego). PUT mantiene comunque id e createdAt dell’attività archiviata.

Prova: invia con POST { "title": " Order new chairs ", "color": "red" } a /api/tasks. Ottieni 400 e formErrors indica Unrecognized key: "color". Invialo di nuovo senza color: ottieni 201 con "title": "Order new chairs" (senza spazi) e "done": false. PATCH /api/tasks/2 con { "done": "yes" } restituisce 400 con una voce done in fieldErrors. PUT /api/tasks/2 con { "title": "Book the big room", "done": false } restituisce 200, con il vecchio createdAt dell’attività 2.

Provalo tu

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

const router = Router();

// Il motivo per cui un title non può essere memorizzato, oppure null se può esserlo.
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;

Tutte le lezioni di API REST

Esercitati da solo: Playground Express