Menu
CoddyTech

Esquemas con zod

Parte de la sección API REST del Journey de Node.js de Coddy. Lección 7 de 18.

Las comprobaciones manuales aumentan con cada campo y cada ruta. Un esquema de zod describe la estructura una sola vez, y safeParse comprueba un valor frente a ella:

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 el cuerpo validado, con name sin espacios al principio ni al final

.strict() rechaza los campos que el esquema no enumera. Event.partial() hace que todos los campos sean opcionales, que es lo que necesita un PATCH, y Event.partial({ seats: true }) solo hace opcional seats. z.flattenError enumera los problemas por campo. Event.parse realiza la misma comprobación, pero lanza una excepción si el valor no es válido, y una excepción en una ruta se convierte en un error 500.

challenge icon

Desafío

Fácil
  • Reemplaza las comprobaciones manuales por un esquema Task: title, una cadena recortada de 1 a 100 caracteres; done, un booleano; .strict().
  • POST (done opcional, valor predeterminado false), PATCH (todos los campos opcionales) y PUT usan safeParse; si el cuerpo no es válido, se responde con el código 400 y z.flattenError(result.error) como error.

Envía una solicitud POST con { "title": " Order new chairs ", "color": "red" } y luego otra sin color.

Pruébalo tú mismo

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

const router = Router();

// El motivo por el que no se puede guardar un título, o null si se puede.
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;

Todas las lecciones de API REST