Validating Input
Part of the REST APIs section of Coddy's Node.js journey. Lesson 6 of 25.
A client can send anything in a body, and if (!name) only stops the empty values. 123, true and ["x"] are truthy and get through. A string of spaces gets through too: catch it with trim(), since ' '.trim() is ''. Check each field's type with typeof, then its limits:
const { seats } = req.body ?? {};
if (typeof seats !== 'number' || !Number.isInteger(seats) || seats < 1) {
return res.status(400).json({ error: 'seats must be a whole number from 1' });
}Write the message for the person who reads the response: name the field and say what it must be, so they know what to fix. A PATCH checks only the fields it was sent, since the others are not changing.
Challenge
EasyRight now if (!title) lets a title like 123 or " " through. Check the type and size of each field before you save it.
- In
routes/tasks.js, in the POST route, checktitle:- It must be a string.
- It must not be empty or only spaces.
- It must be at most 100 characters long, spaces included.
If any of these fails, respond with status 400 and an
errorsentence that contains the wordtitle, for example:{ "error": "title must be a string of 1 to 100 characters" } - In the PATCH route, check only the fields the body sends:
- If it sends
title, the same rule and the same kind of error apply. - If it sends
done, it must betrueorfalse. Otherwise respond with status 400 and an error that contains the worddone:
A refused PATCH changes nothing.{ "error": "done must be true or false" } - If it sends
- Good bodies keep working: POST still returns 201, and PATCH with
{ "done": true }still returns 200. Leave PUT as it is; Schemas with zod checks it too.
Try it: POST { "title": 123 } to /api/tasks: you get 400 with the title error. POST { "title": " " }: 400 as well. PATCH /api/tasks/2 with { "done": "yes" }: 400 with the done error, and GET /api/tasks/2 still shows "done": false. PATCH /api/tasks/2 with { "done": true }: 200.
Try it yourself
import { Router } from 'express';
import { nextId, tasks } from '../store.js';
const router = Router();
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 res.status(404).json({ error: 'Task not found' });
}
res.json(task);
});
router.post('/', (req, res) => {
const { title } = req.body ?? {};
if (!title) {
return res.status(400).json({ error: 'title is required' });
}
const task = { id: nextId(), title, done: false, 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 res.status(404).json({ error: 'Task not found' });
}
const { title, done } = req.body ?? {};
if (title !== undefined) task.title = title;
if (done !== undefined) task.done = done;
res.json(task);
});
router.put('/:id', (req, res) => {
const index = tasks.findIndex((t) => t.id === Number(req.params.id));
if (index === -1) {
return res.status(404).json({ error: 'Task not found' });
}
const { title, done } = req.body ?? {};
if (title === undefined || done === undefined) {
return res.status(400).json({ error: 'PUT needs every field: title and done' });
}
const { id, createdAt } = tasks[index];
tasks[index] = { id, title, done, 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 res.status(404).json({ error: '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 API2Validation and Errors
Validating InputPractice on your own: Express playground