Skip to main content
Back to module
Back-End Web APIs · HTTP and REST

JSON payloads and validation

Reading59mLesson 3 of 3 in this module

Parse JSON request bodies and validate every field before your route uses it.

Why it matters

Your API will receive data from code you did not write: browsers, mobile apps, scripts, and people poking at it with curl. Some of that data will be wrong by accident, and some on purpose. Validation is the habit of checking every incoming value before you use it. It turns a crash or a corrupted database row into a clear 400 response that tells the caller what to fix. It is also your first line of defence against many security bugs.

JSON in one minute

JSON (JavaScript Object Notation) is the standard format for API bodies. It looks like a JavaScript object literal, with stricter rules:

{
  "email": "ada@example.com",
  "age": 36,
  "newsletter": true,
  "interests": ["backend", "databases"],
  "referrer": null
}
  • Keys must be in double quotes. Strings use double quotes too.
  • Values can be strings, numbers, true/false, null, arrays or objects. There is no undefined, no dates, no comments and no trailing commas.
  • Dates travel as strings, usually ISO 8601: "2026-10-23T14:30:00Z".

In JavaScript, JSON.parse(text) turns a JSON string into a value and JSON.stringify(value) does the reverse. JSON.parse throws on invalid input, so a server must catch that and answer 400. Express's express.json() middleware parses the body for you and passes a syntax error to your error handler.

The client should send Content-Type: application/json so the server knows how to read the body:

curl -X POST http://localhost:3000/signup \
  -H "Content-Type: application/json" \
  -d '{"email":"ada@example.com","password":"correct horse battery"}'

The core idea: never trust the body

Parsing only proves that the text is valid JSON. It says nothing about whether the values make sense. After JSON.parse, req.body might be:

  • null, an array, or a number instead of an object
  • missing a required field
  • a field with the wrong type: "age": "thirty"
  • a value out of range: "age": -4
  • a string that is far too long (a 5 MB "name")
  • extra fields you did not ask for, such as "role": "admin"

That last one matters. If a route does db.users.insert(req.body), a caller can set any column they like. This bug is called mass assignment. The fix is to build the object you store from the fields you allow, never from the raw body.

Worked example: a hand-written validator

Here is a validator for a signup body. It returns every problem at once, so the client can show them all:

function validateSignup(body) {
  const errors = [];
  if (body === null || typeof body !== "object" || Array.isArray(body)) {
    return { ok: false, errors: ["Body must be a JSON object."] };
  }
  if (typeof body.email !== "string" || !body.email.includes("@")) {
    errors.push("email must be a valid email address.");
  }
  if (typeof body.password !== "string" || body.password.length < 8) {
    errors.push("password must be at least 8 characters.");
  }
  if (errors.length > 0) return { ok: false, errors };
  // Build the clean value from allowed fields only.
  return { ok: true, value: { email: body.email.trim().toLowerCase(), password: body.password } };
}

And the route uses it before doing anything else:

app.post("/signup", (req, res) => {
  const result = validateSignup(req.body);
  if (!result.ok) return res.status(400).json({ error: "Validation failed.", details: result.errors });
  // From here on, use result.value, never req.body.
});

Note the details: typeof checks before string methods (calling .includes on a number would crash), normalising the email, and returning a new object.

Schema libraries

Writing validators by hand gets repetitive. Most Node projects use a schema library such as zod, Joi or Ajv (which validates against JSON Schema). You describe the shape once and the library checks it:

import { z } from "zod";

const Signup = z.object({
  email: z.string().trim().toLowerCase().email().max(254),
  password: z.string().min(8).max(200),
}).strict(); // reject unknown keys

const result = Signup.safeParse(req.body);
if (!result.success) return res.status(400).json({ error: "Validation failed." });
const { email, password } = result.data;

The same idea applies to query strings and route parameters. req.params.id is always a string, so /courses/abc or /courses/-1 must be rejected before it reaches the database.

Validate on the server, always

Front-end validation (a required attribute, a red border) is for the user's convenience. Anyone can skip it by calling your API directly. Server-side validation is the one that counts. Keep both, but never rely on the front end.

Common mistakes

  • Trusting req.body directly, especially passing it straight to the database.
  • Checking presence but not type. if (body.age) accepts "abc" and rejects 0.
  • No length limits. Huge strings slow the server and fill the database. Cap strings and the total body size.
  • Leaking details. "Expected string at path users[0].hash" may reveal internal structure. Keep messages about the caller's input.
  • Stopping at the first error when the client would rather see all of them.

Quick recap

  • JSON is strict: double quotes, no comments, no undefined. JSON.parse throws on bad input.
  • Parsed is not valid. Check that the body is an object, then check each field's type, range and length.
  • Build the value you use from allowed fields only, to prevent mass assignment.
  • Use a schema library for real projects, and validate params and queries too.

Try it: the Do tab asks you to write validateCourse(body) and see it tested against good and bad bodies.

Also in this lesson: