Conflicts
Part of the REST APIs section of Coddy's Node.js journey. Lesson 13 of 25.
A second task called "Send the invoice" makes the list confusing, but its body is valid. That is a 409 Conflict: the request clashes with what is already stored. A 400 means the body itself is wrong.
const taken = users.some((u) => u.name.toLowerCase() === name.toLowerCase());
if (taken) {
return sendError(res, 409, 'conflict', 'That username is taken');
}Compare in lower case, since people read "Ada" and "ada" as the same name. When a request renames something, skip the item itself in the search. Otherwise a user who saves the form without changing their name gets a conflict with themselves.
Challenge
EasyTwo tasks called "Send the invoice" make the list confusing. Refuse a title that another task already has.
- In
routes/tasks.js, compare titles ignoring case: "Book the meeting room" and "book the MEETING room" count as the same title. - In POST, after the schema check passes: if any task already has the title, use
sendErrorto respond with status 409, and add nothing:{ "error": { "code": "conflict", "message": "A task with this title already exists" } } - Do the same in PATCH (only when its body sends a
title) and in PUT. In these two routes, leave out the task being changed when you search, so a task may keep its own title or change only its capitals. A refused PATCH or PUT changes nothing.
Try it: POST { "title": "book the meeting room" } to /api/tasks: you get 409 with code conflict, because task 2 is called "Book the meeting room". PATCH /api/tasks/3 with { "title": "Write the Project Brief" }: 409, and GET /api/tasks/3 still shows "Send the invoice". PUT /api/tasks/2 with { "title": "Book the meeting room", "done": true }: 200, because that is task 2's own title. POST { "title": "Order new chairs" }: 201.
Try it yourself
import { Router } from 'express';
import { z } from 'zod';
import { sendError, sendInvalid } from '../errors.js';
import { nextId, tasks } from '../store.js';
const router = Router();
// A whole task, as PUT sends it. .strict() refuses fields it does not list.
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;
All lessons in REST APIs
1Resources and Verbs
Resources and URLsCreating with POST4Browsers, Versions and Config
What Is an OriginAllowing Another OriginCORSA Page That Calls the API2Validation and Errors
Validating InputPractice on your own: Express playground