Menu
CoddyTech

Conflictos

Parte de la sección API REST del Journey de Node.js de Coddy. Lección 13 de 25.

Una segunda tarea llamada «Enviar la factura» hace que la lista sea confusa, pero su cuerpo es válido. Eso es un 409 Conflicto: la solicitud entra en conflicto con lo que ya está almacenado. Un 400 significa que el propio cuerpo es incorrecto.

const taken = users.some((u) => u.name.toLowerCase() === name.toLowerCase());
if (taken) {
    return sendError(res, 409, 'conflict', 'That username is taken');
}

Compara en minúsculas, ya que la gente lee «Ada» y «ada» como el mismo nombre. Cuando una solicitud cambia el nombre de algo, omite el propio elemento en la búsqueda. De lo contrario, un usuario que guarda el formulario sin cambiar su nombre tendrá un conflicto consigo mismo.

challenge icon

Desafío

Fácil

Dos tareas llamadas "Send the invoice" hacen que la lista resulte confusa. Rechaza un título que ya tenga otra tarea.

  1. En routes/tasks.js, compara los títulos sin distinguir entre mayúsculas y minúsculas: "Book the meeting room" y "book the MEETING room" cuentan como el mismo título.
  2. En POST, después de que se supere la validación del esquema: si alguna tarea ya tiene ese título, usa sendError para responder con el estado 409, y no añadas nada:
    { "error": { "code": "conflict", "message": "A task with this title already exists" } }
  3. Haz lo mismo en PATCH (solo cuando su cuerpo envíe un title) y en PUT. En estas dos rutas, excluye de la búsqueda la tarea que se está modificando, para que una tarea pueda conservar su propio título o cambiar solo las mayúsculas. Un PATCH o PUT rechazado no cambia nada.

Pruébalo: POST { "title": "book the meeting room" } a /api/tasks: obtienes 409 con el código conflict, porque la tarea 2 se llama "Book the meeting room". PATCH /api/tasks/3 con { "title": "Write the Project Brief" }: 409, y GET /api/tasks/3 sigue mostrando "Send the invoice". PUT /api/tasks/2 con { "title": "Book the meeting room", "done": true }: 200, porque ese es el título de la propia tarea 2. POST { "title": "Order new chairs" }: 201.

Pruébalo tú mismo

import { Router } from 'express';
import { z } from 'zod';
import { sendError, sendInvalid } from '../errors.js';
import { nextId, tasks } from '../store.js';

const router = Router();

// Una tarea completa, tal como la envía PUT. .strict() rechaza los campos que no enumera.
const Task = z
    .object({
        title: z.string().trim().min(1).max(100),
        done: z.boolean(),
    })
    .strict();
const NewTask = Task.partial({ done: true });
const TaskChanges = Task.partial();

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 sendError(res, 404, 'not_found', 'Task not found');
    }
    res.json(task);
});

router.post('/', (req, res) => {
    const result = NewTask.safeParse(req.body);
    if (!result.success) {
        return sendInvalid(res, result.error);
    }
    const { title, done = false } = result.data;
    const task = { id: nextId(), title, done, 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 sendError(res, 404, 'not_found', 'Task not found');
    }
    const result = TaskChanges.safeParse(req.body);
    if (!result.success) {
        return sendInvalid(res, result.error);
    }
    Object.assign(task, result.data);
    res.json(task);
});

router.put('/:id', (req, res) => {
    const index = tasks.findIndex((t) => t.id === Number(req.params.id));
    if (index === -1) {
        return sendError(res, 404, 'not_found', 'Task not found');
    }
    const result = Task.safeParse(req.body);
    if (!result.success) {
        return sendInvalid(res, result.error);
    }
    const { id, createdAt } = tasks[index];
    tasks[index] = { id, ...result.data, 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 sendError(res, 404, 'not_found', 'Task not found');
    }
    tasks.splice(index, 1);
    res.status(204).end();
});

export default router;

Todas las lecciones de API REST

Practica por tu cuenta: Playground de Express