Filtering Lists
Part of the REST APIs section of Coddy's Node.js journey. Lesson 14 of 25.
A client that wants only the open tasks should not download every task and throw most away. The query string, the part after ?, narrows the list on the server:
// GET /api/invoices?paid=false
const { paid } = req.query;
let list = invoices;
if (paid !== undefined) {
list = list.filter((i) => i.paid === (paid === 'true'));
}Query values are always strings. req.query.paid is 'false', and a string that is not empty is truthy, so if (paid) treats ?paid=false like ?paid=true. Compare with the string instead, and answer 400 for a value that is neither 'true' nor 'false'. Each filter narrows what the one before it left, so several filters combine.
Challenge
EasyA client that wants only the open tasks should not download all of them. Let the list filter itself with the query string.
- In
routes/tasks.js, in the list route (GET /api/tasks), readdoneandqfromreq.query. With neither, the route still returns every task. ?done=truereturns only finished tasks, and?done=falseonly open ones. The value arrives as the text"true"or"false", not a boolean. Any other value, like?done=yes, gets status 400:{ "error": { "code": "invalid_query", "message": "done must be true or false" } }?q=roomreturns only tasks whose title containsroom, ignoring case, so it also finds "Pay the ROOM deposit".- Both filters can be used at once:
?done=false&q=roomreturns open tasks withroomin the title.
Try it: GET /api/tasks?done=false returns tasks 2 and 3. GET /api/tasks?q=room returns only task 2, "Book the meeting room". GET /api/tasks?done=true&q=room returns an empty list, []. GET /api/tasks?done=yes returns 400 with code invalid_query.
Try it yourself
import { Router } from 'express';
import { z } from 'zod';
import { sendError, sendInvalid } from '../errors.js';
import { nextId, tasks } from '../store.js';
const router = Router();
// A whole task, as PUT sends it. .strict() refuses fields it does not list.
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();
// True when another task already has this title, in any case.
const titleTaken = (title, exceptId) =>
tasks.some((t) => t.id !== exceptId && t.title.toLowerCase() === title.toLowerCase());
router.get('/', (req, res) => {
res.json(tasks);
});
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;
This lesson includes a short quiz. Start the lesson to answer it and track your progress.
All lessons in REST APIs
1Resources and Verbs
Resources and URLsCreating with POST4Browsers, Versions and Config
What Is an OriginAllowing Another OriginCORSA Page That Calls the APIPractice on your own: Express playground