Menu
CoddyTech

冲突

本课属于 Coddy Node.js 学习旅程的“REST API”单元。第 13 课,共 25 课。

第二个名为“发送发票”的任务让列表变得混乱,但它的请求正文是有效的。这是 409 冲突:请求与已有的存储内容冲突。400 表示请求正文本身有问题。

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

请将名称转换为小写后再进行比较,因为人们会把“Ada”和“ada”看作同一个名字。当请求要重命名某项内容时,请在搜索时跳过该项本身。否则,用户在不更改名字的情况下保存表单,也会与自己发生冲突。

challenge icon

挑战

简单

两个任务都叫 "Send the invoice",会让列表变得混乱。拒绝使用其他任务已有的标题。

  1. 在 routes/tasks.js 中,忽略大小写比较标题:"Book the meeting room" 和 "book the MEETING room" 算作同一个标题。
  2. 在 POST 中,架构检查通过后,如果已有任务使用该标题,就用 sendError 返回状态码 409,并且不添加任何内容:
    { "error": { "code": "conflict", "message": "A task with this title already exists" } }
  3. 在 PATCH(仅当请求体发送了 title 时)和 PUT 中也执行相同的检查。在这两个路由中,搜索时排除正在修改的任务,这样任务可以保留自己的标题,也可以只更改标题中的大小写。拒绝 PATCH 或 PUT 时,不做任何更改。

试试看:向 /api/tasks 发送 POST { "title": "book the meeting room" }:你会得到状态码 409,错误码为 conflict,因为任务 2 的标题是 "Book the meeting room"。使用 { "title": "Write the Project Brief" } 对 PATCH /api/tasks/3 发起请求:返回 409,并且 GET /api/tasks/3 仍会显示 "Send the invoice"。使用 { "title": "Book the meeting room", "done": true } 对 PUT /api/tasks/2 发起请求:返回 200,因为这是任务 2 自己的标题。发送 POST { "title": "Order new chairs" }:返回 201。

亲手试试

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

const router = Router();

// 完整的任务对象,与 PUT 发送的内容一致。.strict() 会拒绝未列出的字段。
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;

REST API中的所有课

自己练习: Express 在线编辑器