Paginasi
Termasuk bagian REST API dalam perjalanan Node.js di Coddy. Pelajaran 17 dari 25.
Daftar berisi sepuluh ribu tugas terlalu panjang untuk satu jawaban. Kirim dalam beberapa halaman: client meminta ?page=2&limit=20 dan menerima potongan itu, beserta informasi yang diperlukan untuk menampilkan "Halaman 2 dari 5":
{ "items": [ ...books 21 to 40... ], "page": 2, "total": 95, "pages": 5 }const start = (page - 1) * limit; // halaman 1 dimulai dari 0
const items = list.slice(start, start + limit);
const pages = Math.ceil(list.length / limit);Hitung total setelah filter diterapkan, karena nilai itu menjelaskan daftar yang diminta client. Jika string kueri tidak memiliki page, gunakan 1, dan 10 untuk limit, sebelum mengonversinya: Number(undefined) adalah NaN. Lalu, tolak nilai apa pun yang bukan bilangan bulat dalam rentang yang diizinkan dengan status 400. Batas maksimum untuk limit mencegah client meminta semuanya sekaligus.
Tantangan
MudahDaftar sepuluh ribu tugas terlalu panjang untuk satu respons. Kirim daftar dalam beberapa halaman, agar client tahu apa yang harus ditampilkan sebagai "Page 1 of 2".
- Di
routes/tasks.js, ubah nilai yang dikembalikan rute daftar: objek, bukan array. UntukGET /api/tasks?limit=2dengan tugas awal, hasilnya adalah:{ "items": [ { "id": 1, "title": "Write the project brief", "done": true, "createdAt": "2026-03-02T09:00:00.000Z" }, { "id": 2, "title": "Book the meeting room", "done": false, "createdAt": "2026-03-03T10:30:00.000Z" } ], "page": 1, "total": 3, "pages": 2 }itemsberisi tugas di halaman ini,pageadalah nomor halaman,totaladalah jumlah tugas yang cocok dengan filter, danpagesadalah jumlah halamannya. - Baca
pagedanlimitdari query string:pageadalah bilangan bulat mulai dari 1. Jika tidak ada, gunakan 1.limitadalah bilangan bulat dari 1 hingga 50. Jika tidak ada, gunakan 10.
Gunakan nilai default sebelum mengonversinya, karena
Number(undefined)adalahNaN. Nilai lainnya (page=0,page=abc,page=1.5,limit=0,limit=51) menghasilkan status 400, dengan pesanlimit must be a whole number from 1 to 50untuk nilai limit yang tidak valid:{ "error": { "code": "invalid_query", "message": "page must be a whole number from 1" } } - Lakukan paginasi terakhir: filter dan urutkan terlebih dahulu, lalu ambil halaman yang diminta.
totalmenghitung jumlah tugas yang tersisa setelah difilter, jadi?done=false&limit=1menghasilkan"total": 2dan"pages": 2.
Ini mengubah bentuk respons, jadi client yang mengharapkan array juga harus berubah. Pelajaran Versioning menunjukkan cara menanganinya.
Coba: GET /api/tasks?limit=2 mengembalikan tugas 1 dan 2 dengan "page": 1, "total": 3, dan "pages": 2. GET /api/tasks?page=2&limit=2 hanya mengembalikan tugas 3. GET /api/tasks biasa mengembalikan semua 3 tugas dengan "page": 1 dan "pages": 1. GET /api/tasks?page=0 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();
const SORT_FIELDS = ['title', 'createdAt'];
// True jika task lain sudah memiliki judul 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, sort } = 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 (sort !== undefined) {
const field = String(sort).startsWith('-') ? String(sort).slice(1) : String(sort);
if (!SORT_FIELDS.includes(field)) {
return sendError(
res,
400,
'invalid_query',
'sort must be title or createdAt, with - in front to reverse',
);
}
const direction = String(sort).startsWith('-') ? -1 : 1;
list = list.toSorted((a, b) => a[field].localeCompare(b[field]) * direction);
}
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;
Pelajaran ini berisi kuis singkat. Mulai pelajarannya untuk menjawab kuis dan melacak progresmu.
Semua pelajaran di REST API
1Sumber Daya dan Kata Kerja
Sumber Daya dan URLMembuat dengan POST4Peramban, Versi, dan Konfigurasi
Apa Itu OriginMengizinkan Origin LainCORSHalaman yang Memanggil APILatihan sendiri: Playground Express