Paginasi
Termasuk bagian REST API dalam perjalanan Node.js di Coddy. Pelajaran 12 dari 18.
Daftar berisi sepuluh ribu tugas terlalu panjang untuk satu jawaban. Kirim dalam beberapa halaman: client meminta ?page=2&limit=20 dan mendapatkan potongan itu, beserta data yang dibutuhkan 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 nilainya menggambarkan daftar yang diminta client. Baca page dan limit dengan Number, 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
MudahGET /api/tasksmemberikan respons{ items, page, total, pages }.?page=adalah bilangan bulat mulai dari 1;?limit=dari 1 hingga 50, nilai default 10.- Selain itu, responsnya adalah 400 dengan kode
invalid_query.
Kirim GET /api/tasks?limit=2, GET /api/tasks?page=2&limit=2, lalu GET /api/tasks?page=0.
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'];
// 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, 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).replace(/^-/, '');
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 POSTLatihan sendiri: Playground Express