עימוד
חלק מהיחידה REST APIs במסלול ה-Node.js של Coddy. שיעור 12 מתוך 18.
רשימה של עשרת אלפים משימות ארוכה מדי לתשובה אחת. שלחו אותה בעמודים: הלקוח מבקש ?page=2&limit=20 ומקבל את החלק הזה, יחד עם מה שדרוש לו כדי להציג "עמוד 2 מתוך 5":
{ "items": [ ...books 21 to 40... ], "page": 2, "total": 95, "pages": 5 }const start = (page - 1) * limit; // עמוד 1 מתחיל ב-0
const items = list.slice(start, start + limit);
const pages = Math.ceil(list.length / limit);חשבו את total אחרי הסינון, כי הוא מתאר את הרשימה שהלקוח ביקש. קראו את page ואת limit באמצעות Number ודחו כל ערך שאינו מספר שלם בטווח, עם קוד 400. הגבלה מרבית על limit מונעת מלקוח לבקש הכול בבת אחת.
אתגר
קלGET /api/tasksמחזיר{ items, page, total, pages }.?page=הוא מספר שלם החל מ־1;?limit=בין 1 ל־50, ברירת המחדל היא 10.- כל ערך אחר מחזיר תשובת 400 עם הקוד
invalid_query.
שלחו GET /api/tasks?limit=2, אחר כך GET /api/tasks?page=2&limit=2, ואז GET /api/tasks?page=0.
נסו בעצמכם
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();
const SORT_FIELDS = ['title', 'createdAt'];
// True אם למשימה אחרת כבר יש את הכותרת הזו, ללא תלות באותיות גדולות וקטנות.
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;
השיעור הזה כולל חידון קצר. התחילו את השיעור כדי לענות עליו ולעקוב אחרי ההתקדמות.