Menu
CoddyTech

التعارضات

جزء من قسم واجهات REST API في رحلة Node.js على Coddy. الدرس 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 المرفوض أي شيء.

جرّب ذلك: أرسل POST { "title": "book the meeting room" } إلى /api/tasks: ستحصل على 409 مع الرمز conflict، لأن اسم المهمة 2 هو "Book the meeting room". أرسل PATCH /api/tasks/3 مع { "title": "Write the Project Brief" }: ستحصل على 409، وسيظل GET /api/tasks/3 يعرض "Send the invoice". أرسل PUT /api/tasks/2 مع { "title": "Book the meeting room", "done": true }: ستحصل على 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

تدرّب بنفسك: Playground لـ Express