PLAINFINCH learn by building

Checking what people send

You will check what people send to your server and answer with a clear list of problems when something is wrong.

Why can’t the server trust what arrives?

Anything a browser can send, a person can send by hand. Your booking form may have a required attribute and a drop-down of three services, but those only shape what a polite browser does. Someone can open the developer tools, delete the attribute and submit. Someone else can skip the page entirely and send a request with a terminal command or a short script. The request bar on this page does exactly that.

The server is the only place you control. Whatever arrives in req.body, req.query or req.params came from outside, and it can be missing, empty, the wrong type, absurdly long, or full of text designed to cause trouble. Checking it on the server is called validation, and every route that accepts data needs it.

Checks in the page are still worth having. They give people instant feedback while they type, which is kinder than a round trip. Treat them as a convenience for honest visitors, and treat the server checks as the rules.

What does the starting server get wrong?

Send **Empty name**. The server answers 201 Created and stores a booking with an empty name. Send **Unknown service** and it happily books a paint job, which the bike shop doesn’t offer. Send **Empty booking** and it saves a repair with no name and no service at all. Then send GET /api/repairs and look at the list: every one of those is now a real booking the mechanics would have to deal with.

Nothing crashed. That is the dangerous part. A server that accepts bad data looks healthy in the Log and returns happy status codes, and the problem only shows up later, when the repair list has blank rows or a page tries to print a name that isn’t there.

What should you check about each field?

Go field by field and ask four questions. Each one turns into a line or two of code.

const body = req.body || {};
const name = typeof body.name === 'string' ? body.name.trim() : '';
const errors = [];

if (!name) errors.push('name is required');
else if (name.length > 60) errors.push('name must be 60 characters or fewer');
if (!SERVICES.includes(body.service)) {
  errors.push('service must be one of: ' + SERVICES.join(', '));
}
  • Is it there? A missing field is undefined. An empty one is ''. Both usually mean the same thing to you.
  • Is it the right type? JSON can carry strings, numbers, booleans, arrays and objects. A name of 42 or ["Ada"] is not a name.
  • Is it a sensible size? Set a maximum length for text, and a range for numbers. A 50,000 character name is a mistake or an attack.
  • Is it an allowed value? When only a few values make sense, like the three services, check against that list instead of trying to describe what a bad value looks like.

Why trim text before checking it?

A name of four spaces passes if (!name), because a string with spaces in it is not empty. People also paste text with a stray space at the end, or type Ada and press Enter. Calling trim() removes spaces, tabs and line breaks from both ends, so you check the text people meant to send.

Trim before the check and save the trimmed value, so the stored booking says Ada, not Ada . Only call trim() once you know the value is a string. If someone sends "name": 42, then body.name.trim() throws a TypeError and the visitor gets a 500. The typeof test in the example turns anything that isn’t a string into '', which the next line reports as missing.

Leave passwords alone. A space at the start of a password may be deliberate, and changing it would lock the person out.

What should a 400 answer look like?

Status 400 Bad Request means the client sent something the server won’t accept, and sending the same request again won’t help. The body should say what is wrong in words a person or a page can use. For a JSON API, an object with an errors array is easy to read and easy to show next to a form.

Collect every problem before answering. If the booking has no name and an unknown service, say both. Answering with only the first error makes people fix one thing, send again, and discover the next, which is slow and irritating. The pattern is always the same: start with an empty array, push a message for each failed check, and answer 400 if the array isn’t empty.

Be specific without leaking internals. service must be one of: puncture, brakes, full-service helps. A database error message or a stack trace doesn’t, and it tells an attacker how your server is built.

if (errors.length > 0) {
  return res.status(400).json({ errors });
}
// only valid bookings reach this line
repairs.push({ id: repairs.length + 1, name, service: body.service });
res.status(201).json(repairs[repairs.length - 1]);
FigurePOST /api/repairs with name empty and service brakes. The name check fails and adds name is required; the service check passes. Errors are not empty, so the server returns 400 with the errors list and nothing is saved. A good booking gets 201.POST /api/repairs with name empty and service brakes. The name check fails and adds name is required; the service check passes. Errors are not empty, so the server returns 400 with the errors list and nothing is saved. A good booking gets 201.
Every check runs and adds its message to errors, so one 400 answer can list every problem at once. Only a booking with no errors reaches repairs.push and the 201.

Why does the early answer need return?

Calling res.status(400).json(...) sends the answer, but it doesn’t stop your function. JavaScript carries on to the next line, saves the bad booking and calls res.status(201).json(...). That second answer can’t be sent, because the first one already went out, and Express prints ERR_HTTP_HEADERS_SENT in the Log. The bad data is saved anyway.

Writing return res.status(400).json(...) answers and leaves the function in one step. Read the Log after you send a bad request. If you see ERR_HTTP_HEADERS_SENT, look for an answer without a return in front of it.

What happens when the JSON itself is broken?

express.json() reads the body before your route runs. If the text isn’t valid JSON, such as {"name": "Ada" with the closing brace missing, it never reaches your code. Express answers 400 on its own and prints the SyntaxError in the Log. You can try it from the request bar by typing a broken body.

In Express 5, req.body is undefined when no body parser handled the request, for example when the client sends a form or no Content-Type at all. Destructuring undefined throws, which is why the solution starts with const body = req.body || {}. The missing fields then show up as ordinary validation errors instead of a 500.

What do AI tools get wrong about validation?

Generated routes often skip validation entirely and pass req.body straight into a database or an array. When they do validate, watch for checks that test only whether a field exists, never its type or length; if (quantity < 1) style comparisons that let strings slip through; and an early res.status(400) with no return. Also check that the validation lives on the server as well as in the page’s JavaScript.

For larger projects people often use a validation library that describes the shape of the data in one place. That is a fine choice once you know what the checks have to do. The hand-written version here teaches exactly that, and for a few fields it is short and clear.

Field notes

  • Anyone can send anything, so the server must check every field itself.
  • Ask of each field: is it there, is it the right type, and is it an allowed value.
  • Trim text before checking it, so a name made of spaces counts as empty.
  • Answer 400 with every problem listed, and return so the bad data is never saved.

Easy to get wrong

Without return, the function keeps running after the 400, saves the bad data and tries to answer twice. The Log shows ERR_HTTP_HEADERS_SENT.

Comparing text with a number gives false, so "lots" < 1 lets the string through. Check the type first, then the range.

If name is missing or is a number, calling trim() on it throws and the visitor gets a 500 instead of a helpful 400.

Browser checks help honest visitors but anyone can skip them by sending a request directly. Only the server’s checks are rules.

Questions people ask

Should I use 400 or 422 for invalid data?

Both are used. 400 Bad Request is the general answer for a request the server refuses because of what the client sent. 422 Unprocessable Content is a more specific code some APIs use when the JSON is well formed but the values fail the rules. Pick one and use it consistently; this course uses 400.

Should I fix bad data without telling the user instead of rejecting it?

Small, obvious clean-ups such as trimming spaces or lower-casing an email address are fine. Guessing what someone meant, like turning "lots" into 1, is not. Reject it and say why, so the sender can correct it.

Does validation protect against SQL injection and unsafe HTML?

It helps, because tight rules leave less room for odd input. It isn’t enough on its own. Use ? placeholders for SQL and escape text before putting it in HTML, even for values that passed validation. Later lessons cover both.

Where should the list of allowed values live?

In one place on the server, like the SERVICES array here, so the check and any page that shows the options read the same list. When the list grows or changes often, it usually moves into the database.

Keep going