Paginação
Parte da seção APIs REST do Journey de Node.js da Coddy. Lição 17 de 25.
Uma lista de dez mil tarefas é longa demais para uma única resposta. Envie-a em páginas: o cliente solicita ?page=2&limit=20 e recebe esse trecho, além do que precisa para exibir “Página 2 de 5”:
{ "items": [ ...books 21 to 40... ], "page": 2, "total": 95, "pages": 5 }const start = (page - 1) * limit; // a página 1 começa em 0
const items = list.slice(start, start + limit);
const pages = Math.ceil(list.length / limit);Calcule total depois dos filtros, pois ele descreve a lista solicitada pelo cliente. Quando a string de consulta não tiver page, use 1 e 10 para limit antes de fazer a conversão: Number(undefined) é NaN. Em seguida, rejeite com o código 400 qualquer valor que não seja um número inteiro dentro do intervalo. Um limite máximo para limit impede que um cliente solicite tudo de uma só vez.
Desafio
FácilUma lista de dez mil tarefas é longa demais para uma única resposta. Envie a lista em páginas, com o que um cliente precisa para mostrar "Page 1 of 2".
- Em
routes/tasks.js, altere o que a rota de lista retorna: um objeto em vez de um array. ParaGET /api/tasks?limit=2com as tarefas iniciais, ela retorna:{ "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 }itemscontém as tarefas desta página,pageé o número da página,totalé o número de tarefas que correspondem aos filtros epagesé o número total de páginas. - Leia
pageelimitda string de consulta:pageé um número inteiro a partir de 1. Se não for informado, use 1.limité um número inteiro de 1 a 50. Se não for informado, use 10.
Use o valor padrão antes de converter, porque
Number(undefined)éNaN. Qualquer outro valor (page=0,page=abc,page=1.5,limit=0,limit=51) retorna o status 400, com a mensagemlimit must be a whole number from 1 to 50para um limite inválido:{ "error": { "code": "invalid_query", "message": "page must be a whole number from 1" } } - Faça a paginação por último: filtre e ordene primeiro, depois extraia a página.
totalconta as tarefas restantes após os filtros, então?done=false&limit=1retorna"total": 2e"pages": 2.
Isso altera o formato da resposta, então um cliente que esperava um array também precisa ser alterado. A lição sobre versionamento mostra como lidar com isso.
Experimente: GET /api/tasks?limit=2 retorna as tarefas 1 e 2, com "page": 1, "total": 3 e "pages": 2. GET /api/tasks?page=2&limit=2 retorna apenas a tarefa 3. GET /api/tasks sem parâmetros retorna as 3 tarefas, com "page": 1 e "pages": 1. GET /api/tasks?page=0 retorna 400 com o código invalid_query.
Experimente você mesmo
import { Router } from 'express';
import { z } from 'zod';
import { sendError, sendInvalid } from '../errors.js';
import { nextId, tasks } from '../store.js';
const router = Router();
// Uma tarefa completa, como PUT a envia. .strict() rejeita campos que não estão listados.
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'];
// Verdadeiro quando outra tarefa já tem esse título, independentemente de maiúsculas ou minúsculas.
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;
Esta lição inclui um quiz rápido. Comece a lição para respondê-lo e acompanhar seu progresso.
Todas as lições de APIs REST
1Recursos e verbos
Recursos e URLsCriando com POST4Navegadores, versões e configuração
O que é uma origemPermitindo outra origemCORSUma página que chama a APIPratique por conta própria: Playground de Express