Course outline

Express.js / Basic: request and response ownership / Lesson 5

Body parsing, validation and safe projection

Treat parsing as syntax recognition and build an explicit bounded domain input before writing.

About 5 min readContent reviewed
On this page

Lesson goals

  • Separate media type, parser limits, structural validation and domain rules.
  • Reject unknown or malformed values without mass-assigning request bodies.

Parsed JSON is still untrusted application input

express.json recognizes supported JSON input according to its options and matching media type. It does not prove that title exists, is a string, meets a length rule or is allowed to change for this principal. In Express 5, req.body can remain undefined when no parser populated it, so handle parser applicability explicitly. Distinguish malformed JSON, unsupported content type, excessive size and valid JSON with invalid domain structure. A consistent error contract should classify these outcomes without exposing parser internals or accidentally treating a missing body as an empty valid update.

Scope the parser and the write boundary

This fragment mounts a bounded JSON parser on a specific write route and then converts the body through a validation function. The size is a synthetic teaching choice, not a universal safe limit. A real application must also enforce appropriate limits at its ingress and account for decompression and upstream buffering. The service receives a deliberately selected domain input rather than req.body directly. Authentication and object authorization must already be established before a permitted write. The route is a non-runnable framework illustration; the separate local validator supplies only the claimed pure-function test evidence.

javascript Illustrative parse-validate-write sequence
app.post("/courses", express.json({limit:"8kb"}), async (req,res) => {
  const input = validateCourseInput(req.body);
  const course = await catalog.createFor(req.principal,input);
  return res.status(201).json({id:course.id,title:course.title});
});

The fragment assumes a reviewed principal and injected catalog. Validation must finish before createFor, and the response exposes selected fields only.

Choose unknown-field and normalization rules explicitly

Copying every body member into an entity can let a caller change role, owner or publication state through fields the endpoint never intended to expose. Define allowed keys and project only them. Decide whether unknown members are rejected or ignored, and document how trimming, Unicode length and empty strings behave. A validation rule should not silently change identity or permission. For patch endpoints, member absence, explicit null and an empty string can mean different actions. Test those cases separately rather than using a truthiness shortcut that makes omission and clearing indistinguishable.

Build a small strict domain input

The offline validator requires a plain object-like JSON record with exactly one allowed member and a trimmed title of one to sixty Unicode code points. Arrays, null and unknown keys are rejected. Counting code points is an explicit lab policy, not a claim to count user-perceived grapheme clusters or database bytes. It returns a new object so a source service receives a narrow contract. The test covers valid normalization, unknown-field rejection and blank input rejection. Real applications may need a richer reviewed schema library and additional domain checks, but should preserve these explicit boundary decisions.

javascript Complete offline input validator
function validateCourseInput(body) {
  if (!body || typeof body !== "object" || Array.isArray(body)) throw new Error("INVALID_INPUT");
  if (Object.keys(body).some(key => key !== "title")) throw new Error("UNKNOWN_FIELD");
  if (typeof body.title !== "string") throw new Error("INVALID_TITLE");
  const title = body.title.trim();
  if ([...title].length < 1 || [...title].length > 60) throw new Error("INVALID_TITLE");
  return {title};
}
console.assert(validateCourseInput({title:"  HTTP foundations  "}).title === "HTTP foundations");
for (const body of [{title:"X",owner:"other"},{title:"   "},null,[]]) {
  let rejected=false; try {validateCourseInput(body);} catch {rejected=true;}
  console.assert(rejected);
}

The valid title is trimmed. All four invalid fixtures throw before a domain write exists; the model does not execute express.json.

Preserve raw bytes when the protocol requires them

Some signed callback protocols authenticate the original body bytes. Parsing and reserializing JSON can change spacing or representation while preserving the same logical object, so it is not a valid substitute for those raw bytes. Choose a scoped raw-body or verification integration documented for the specific protocol and ensure another parser did not already consume the stream. Do not log signatures, secrets or full private payloads while diagnosing verification. Syntax validation, signature checking, replay protection and domain authorization each establish different facts and need their own bounded failure behavior.

Common errors and fixes

Valid JSON changes an internal owner field

Why it happens
The body was mass-assigned instead of projected into allowed input.
How to fix it
Define allowed keys and pass a new narrow object to the domain.
Prevent it next time
Test unknown sensitive members such as owner or role.

req.body is undefined

Why it happens
No applicable parser populated it, or parser placement was wrong.
How to fix it
Check method, media type, parser scope and ordering before assuming an empty object.
Prevent it next time
Test absent bodies and unsupported media types explicitly.

A signed payload fails after JSON reconstruction

Why it happens
Verification used reconstructed text rather than original protocol bytes.
How to fix it
Preserve verified raw-body access under the specific integration contract.
Prevent it next time
Test whitespace variations and parser ordering with synthetic signed fixtures.

Check your understanding

1. Why does the local title rule use code points rather than claiming to count visible characters?

Show worked solution

JavaScript spreading a string iterates code points, while user-perceived characters can contain multiple code points. Storage may also impose byte limits. State which measure the contract uses and test representative multilingual text. Do not claim that one short local length check satisfies every display, normalization or persistence requirement.

2. Does JSON parsing authenticate a signed callback or authorize its state change?

Show worked solution

No. Parsing recognizes syntax and creates values. Signature verification binds the required original bytes to the expected sender under its protocol, replay controls address repeated delivery, and domain policy decides whether the requested transition is allowed. Keep those stages explicit and bounded instead of letting a successful parse stand in for all of them.

Key takeaways

  • Parsing recognizes syntax; validation and policy decide permissible input.
  • A narrow projected input prevents accidental mass assignment.

Official references