Menu
CoddyTech

Sắp xếp

Thuộc phần REST API trong hành trình Node.js của Coddy. Bài học 16 trên 25.

Client muốn xem các nhiệm vụ mới nhất trước tiên, hoặc danh sách theo thứ tự từ A đến Z. Một quy ước phổ biến là ?sort=field, thêm - phía trước để đảo ngược thứ tự:

// GET /api/books?sort=-year
const field = sort.startsWith('-') ? sort.slice(1) : sort;   // 'year'
const direction = sort.startsWith('-') ? -1 : 1;
list = list.toSorted((a, b) => (a[field] - b[field]) * direction);

Trước tiên, hãy kiểm tra field có nằm trong các trường bạn cho phép không, và trả về 400 nếu không, vì trường không có trong sách sẽ không giúp sắp xếp hữu ích. toSorted trả về một bản sao đã sắp xếp. sort sắp xếp lại chính mảng đó. Nếu không có bộ lọc nào tạo ra bản sao, mảng đó chính là kho dữ liệu: mọi yêu cầu sau đều thấy thứ tự mới. Ví dụ này dùng phép trừ, chỉ áp dụng được cho số. Để so sánh hai chuỗi, hãy dùng a.localeCompare(b): giống như phép trừ, cách này trả về một số âm, 0 hoặc một số dương. Ngày ISO là các chuỗi được sắp xếp theo thứ tự thời gian.

challenge icon

Thử thách

Dễ

Thay thế cách sắp xếp chỉ theo tiêu đề mà bạn đã viết trong phần Sắp xếp văn bản bằng cách xử lý trường và thứ tự bên dưới.

Ứng dụng khách muốn xem các tác vụ mới nhất trước, hoặc danh sách theo thứ tự từ A đến Z. Để danh sách tự sắp xếp bằng ?sort=.

  1. Trong routes/tasks.js, ở tuyến danh sách, đọc sort từ req.query. Nếu không có, danh sách giữ nguyên thứ tự đã lưu.
  2. ?sort=title sắp xếp theo tiêu đề từ A đến Z. ?sort=createdAt sắp xếp theo thời gian tạo, cũ nhất trước. Thêm - ở phía trước sẽ đảo ngược thứ tự: ?sort=-title là từ Z đến A, còn ?sort=-createdAt là mới nhất trước.
  3. Cả hai trường đều là văn bản. createdAt là ngày tháng theo định dạng ISO như "2026-03-03T10:30:00.000Z", nên có thể sắp xếp chính xác dưới dạng văn bản. Vì vậy, hãy so sánh bằng localeCompare, không dùng phép trừ.
  4. Trường bất kỳ khác, chẳng hạn ?sort=color, sẽ trả về mã trạng thái 400:
    { "error": { "code": "invalid_query", "message": "sort must be title or createdAt, with - in front to reverse" } }
  5. Sắp xếp sau khi lọc, để ?done=false&sort=-title trả về các tác vụ đang mở theo thứ tự từ Z đến A.
  6. Sắp xếp một bản sao bằng toSorted. sort sắp xếp lại chính mảng đã lưu, khiến mọi yêu cầu tiếp theo đều thấy thứ tự mới.

Thử nhé: GET /api/tasks?sort=title trả về các tác vụ 2, 3, 1 ("Book...", "Send...", "Write..."). GET /api/tasks?sort=-createdAt trả về 3, 2, 1. Sau đó, GET /api/tasks thông thường trả về 1, 2, 3 như cũ. GET /api/tasks?sort=color trả về mã 400 với mã lỗi invalid_query.

Tự làm thử

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

const router = Router();

// Một tác vụ đầy đủ như PUT gửi đến. .strict() từ chối các trường không có trong danh sách.
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();

// True nếu một tác vụ khác đã có tiêu đề này, không phân biệt chữ hoa chữ thường.
const titleTaken = (title, exceptId) =>
    tasks.some((t) => t.id !== exceptId && t.title.toLowerCase() === title.toLowerCase());

router.get('/', (req, res) => {
    const { done, q } = req.query;
    let list = tasks;
    if (done !== undefined) {
        if (done !== 'true' && done !== 'false') {
            return sendError(res, 400, 'invalid_query', 'done must be true or false');
        }
        list = list.filter((t) => t.done === (done === 'true'));
    }
    if (q !== undefined) {
        const words = String(q).toLowerCase();
        list = list.filter((t) => t.title.toLowerCase().includes(words));
    }
    if (req.query.sort === 'title') {
        list = list.toSorted((a, b) => a.title.localeCompare(b.title));
    }
    res.json(list);
});

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;
    if (titleTaken(title)) {
        return sendError(res, 409, 'conflict', 'A task with this title already exists');
    }
    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);
    }
    if (result.data.title !== undefined && titleTaken(result.data.title, task.id)) {
        return sendError(res, 409, 'conflict', 'A task with this title already exists');
    }
    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];
    if (titleTaken(result.data.title, id)) {
        return sendError(res, 409, 'conflict', 'A task with this title already exists');
    }
    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;

Tất cả bài học trong REST API

Tự luyện tập: Playground Express