Schémas avec zod
Fait partie de la section API REST du Journey Node.js de Coddy. Leçon 11 sur 25.
Les vérifications manuelles se multiplient avec chaque champ et chaque route. Un schéma zod décrit la structure une seule fois, et safeParse teste une valeur par rapport à ce schéma :
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 contient le corps de la requête validé, avec name sans espaces en début ni en fin.strict() refuse les champs que le schéma ne répertorie pas. Event.partial() rend chaque champ facultatif, ce dont une requête PATCH a besoin. Event.partial({ seats: true }) rend uniquement seats facultatif, et const { seats = 1 } = result.data utilise 1 si le champ est absent. z.flattenError(result.error) transforme les problèmes en { formErrors, fieldErrors } : formErrors contient les problèmes concernant l’ensemble du corps, comme un champ inconnu, et fieldErrors contient les problèmes de chaque champ. Event.parse effectue la même vérification, mais lève une exception si la valeur est invalide ; une exception dans une route entraîne une réponse 500.
Défi
FacileCette étape ouvre le projet d’API de suivi des tâches préparé pour le cours, plutôt que les fichiers de votre projet de Validation des entrées (y compris routes/tasks.js). Votre travail précédent reste dans sa propre leçon. Lisez les fichiers fournis avant de modifier routes/tasks.js. Cette copie contient des routes CRUD pour les tâches et une validation des entrées écrite à la main.
Vos vérifications écrites à la main se multiplient avec chaque champ et chaque route. Décrivez une fois une tâche avec un schéma zod, puis utilisez-le dans les trois routes qui acceptent un corps de requête.
- Dans
routes/tasks.js, importezzdepuiszodet écrivez un schéma appeléTask:title: une chaîne, débarrassée des espaces de début et de fin, de 1 à 100 caractères après suppression des espaces.done:trueoufalse(z.boolean()).- Le schéma est strict : tout champ qui n’y figure pas, comme
color, est refusé.
Supprimez l’assistant fourni
titleError(la version du cours de la validation de Validation des entrées). - Créez deux versions de
Taskavec.partial(). La version de POST rend uniquementdonefacultatif. La version de PATCH rend les deux champs facultatifs. PUT utiliseTasklui-même ; il lui faut donc les deux champs. - Dans POST, PATCH et PUT, vérifiez
req.bodyavecsafeParse. Si la vérification échoue, renvoyez le statut 400 et la liste des problèmes de zod souserror, créée avecz.flattenError(result.error). Pour{ "title": 123 }, le résultat ressemble à ceci :{ "error": { "formErrors": [], "fieldErrors": { "title": ["Invalid input: expected string, received number"] } } } - Si la vérification réussit, enregistrez les données à partir de
result.data, et non dereq.body: elles contiennent le titre sans espaces superflus. Dans POST, un corps sansdonecrée la tâche avec"done": false(un champ manquant estundefineddansresult.data, et??fournit une valeur de repli). PUT conserve toujoursidetcreatedAtde la tâche stockée.
À essayer : envoyez { "title": " Order new chairs ", "color": "red" } à /api/tasks avec POST. Vous obtenez 400, et formErrors indique Unrecognized key: "color". Renvoyez la requête sans color : vous obtenez 201 avec "title": "Order new chairs" (sans espaces) et "done": false. PATCH /api/tasks/2 avec { "done": "yes" } renvoie 400 avec une entrée done dans fieldErrors. PUT /api/tasks/2 avec { "title": "Book the big room", "done": false } renvoie 200, avec l’ancien createdAt de la tâche 2.
Essayez vous-même
import { Router } from 'express';
import { nextId, tasks } from '../store.js';
const router = Router();
// La raison pour laquelle title ne peut pas être enregistré, ou null s'il peut l'être.
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;
Toutes les leçons de API REST
1Ressources et verbes
Ressources et URLCréer avec POST4Navigateurs, versions et configuration
Qu’est-ce qu’une origineAutoriser une autre origineCORSUne page qui appelle l’API2Validation et erreurs
Valider les entréesEntraînez-vous par vous-même : Playground Express