Menu
CoddyTech

Pengurutan

Termasuk bagian REST API dalam perjalanan Node.js di Coddy. Pelajaran 16 dari 25.

client menginginkan tugas terbaru di urutan pertama, atau daftar dari A sampai Z. Konvensi yang umum adalah ?sort=field, dengan - di depan untuk membalik urutannya:

// 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);

Periksa field terhadap field yang kamu izinkan terlebih dahulu, lalu berikan jawaban 400 untuk field lainnya, karena field yang tidak dimiliki buku tidak menghasilkan urutan yang berguna. toSorted mengembalikan salinan yang sudah diurutkan. sort mengurutkan ulang array itu sendiri. Jika tidak ada filter yang membuat salinan, array tersebut adalah penyimpanan data: setiap permintaan berikutnya akan melihat urutan baru. Contoh ini menggunakan pengurangan, yang hanya berfungsi untuk angka. Untuk membandingkan dua string, gunakan a.localeCompare(b): seperti pengurangan, fungsi ini mengembalikan angka negatif, 0, atau angka positif. Tanggal ISO adalah string yang akan diurutkan berdasarkan urutan waktu.

challenge icon

Tantangan

Mudah

Ganti pengurutan yang hanya berdasarkan judul yang kamu tulis di Sorting Text dengan penanganan field dan arah pengurutan di bawah ini.

Client ingin tugas terbaru ditampilkan lebih dulu, atau list diurutkan dari A sampai Z. Biarkan list mengurutkan dirinya dengan ?sort=.

  1. Di routes/tasks.js, pada route list, baca sort dari req.query. Tanpanya, list mempertahankan urutan penyimpanannya.
  2. ?sort=title mengurutkan berdasarkan judul dari A sampai Z. ?sort=createdAt mengurutkan berdasarkan waktu pembuatan, dari yang paling lama. Tanda - di depan membalik urutan: ?sort=-title mengurutkan dari Z ke A, dan ?sort=-createdAt menampilkan yang terbaru lebih dulu.
  3. Kedua field berupa teks. createdAt adalah tanggal ISO seperti "2026-03-03T10:30:00.000Z", yang diurutkan dengan benar sebagai teks. Jadi, bandingkan dengan localeCompare, bukan dengan pengurangan.
  4. Field lain apa pun, seperti ?sort=color, menghasilkan status 400:
    { "error": { "code": "invalid_query", "message": "sort must be title or createdAt, with - in front to reverse" } }
  5. Lakukan pengurutan setelah filter, agar ?done=false&sort=-title mengembalikan tugas yang belum selesai dari Z ke A.
  6. Urutkan salinan dengan toSorted. sort mengubah urutan array yang tersimpan itu sendiri, dan setiap permintaan berikutnya akan melihat urutan baru tersebut.

Coba: GET /api/tasks?sort=title mengembalikan tugas 2, 3, 1 ("Book...", "Send...", "Write..."). GET /api/tasks?sort=-createdAt mengembalikan 3, 2, 1. Lalu, GET /api/tasks biasa mengembalikan 1, 2, 3 lagi. GET /api/tasks?sort=color mengembalikan 400 dengan kode invalid_query.

Coba sendiri

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

const router = Router();

// Task lengkap, seperti yang dikirim oleh PUT. .strict() menolak field yang tidak tercantum.
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();

// Bernilai true jika task lain sudah memiliki title ini, tanpa membedakan huruf besar dan kecil.
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;

Semua pelajaran di REST API

Latihan sendiri: Playground Express