What is true of everything in req.query
toPositiveInt(value, fallback) existed because every value arriving in req.query is a raw string sent by an untrusted client.
Everything a client sends arrives as untrusted text. Query params, headers, and path params are strings until you convert them, and any of them can be hostile or nonsense.
Both halves of that sentence carry weight. The string part causes correctness bugs like NaN arithmetic, and the untrusted part causes security bugs, and one conversion-with-validation at the edge addresses both.
This lesson extends the same discipline to request bodies, which arrive as JSON text. A body is larger and more structured than a query parameter and is exactly as untrustworthy.
The data format of the web
APIs exchange JSON (JavaScript Object Notation): a text format for objects, arrays, strings, numbers, booleans, and null. It looks like JavaScript literals with two extra rules: keys are always double-quoted, and no trailing commas, comments, or functions.
Networks only carry bytes of text, so objects must be converted both ways:
JSON.stringify(obj)turns a live object into a JSON string (to send).JSON.parse(text)turns a JSON string back into an object (to receive).
Express's res.json(obj) calls stringify for you, and express.json() middleware calls parse on incoming bodies. Underneath, it is these two functions.
A JSON round trip
An object becomes a wire string and comes back as a real object, with working properties and arrays.
const user = { id: 7, name: "Ada", tags: ["admin"] }; const wire = JSON.stringify(user); console.log(wire); const back = JSON.parse(wire); console.log(back.name); console.log(back.tags.length);
Output
{"id":7,"name":"Ada","tags":["admin"]}
Ada
1wire is a plain string, and back is a fresh object built from that string. They contain the same information in two forms, and only one of them can travel over a network.
The output shows the two extra JSON rules in action. Every key is double-quoted, including id, and the numbers and array survive as numbers and an array rather than becoming text.
back is a new object rather than the original, so back === user is false while their contents match. That is what makes stringify-then-parse a common deep-copy trick, and also why it silently drops anything JSON cannot represent.
The list of what gets dropped is worth knowing before it surprises you. Functions and undefined values disappear, Date objects become strings, and NaN and Infinity become null, which is why an API's dates arrive as text needing conversion.
Parse can explode
JSON.parse throws an exception on malformed input, and clients send malformed JSON all the time (truncated requests, hand-written curl commands, buggy apps). An unhandled throw in a server means a crashed request, so parsing untrusted text always gets a try/catch:
try { const body = JSON.parse(text); } catch (err) { // respond 400 invalid_input, using the error shape from lesson 4-3 }
A tidy pattern wraps this in a helper that never throws and returns a result object instead. Build it below, this shape ({ ok, value } or { ok, error }) appears all over production Node code.
A parse that never throws
safeParse(text) returns a result object either way, so callers branch on data instead of catching exceptions.
function safeParse(text) { try { return { ok: true, value: JSON.parse(text) }; } catch (err) { return { ok: false, error: "invalid json" }; } } console.log(JSON.stringify(safeParse('{"page":2}'))); console.log(JSON.stringify(safeParse("{page: 2}")));
Output
{"ok":true,"value":{"page":2}}
{"ok":false,"error":"invalid json"}The try block returns the success object and the catch block returns the failure object, and nothing after a return runs, so each branch is a single line.
The second input uses an unquoted key, which is legal JavaScript and illegal JSON. That mismatch catches people constantly, since a body that looks fine pasted into an editor is rejected by the parser.
The { ok, value } or { ok, error } shape appears all over production Node code, and its advantage is that failure becomes ordinary control flow. A caller writes if (!result.ok) return badRequest() instead of wrapping every call site in its own try.
It also puts the decision in the right place. safeParse knows how to parse and not what a failure should mean, so the handler decides whether an invalid body is a 400, a default value, or a logged warning.
Note that err is caught and ignored here, which is defensible for JSON syntax errors since the message rarely helps a client. Logging it server-side is still worth doing, because a sudden spike of parse failures usually means a client shipped a bug.
A lying Content-Type header
When a service sends Content-Type: application/json with the body Hello, calling res.json() means the parse throws, because "Hello" is not valid JSON no matter what the header claims.
Headers are promises rather than guarantees. The body is whatever bytes the server actually sent, and parsing is where the truth comes out.
Bare Hello is not valid JSON, since a JSON string would be "Hello" with the quotes included. That distinction is easy to miss, and it means a text response is never accidentally valid JSON unless it happens to be a bare number.
The situation is common in a specific way worth predicting. A crashing service often returns an HTML error page from a proxy while the route's declared content type stays JSON, so the parse error you see mentions an unexpected < character.
This is why both sides of the wire wrap parsing, with your server guarding incoming bodies and well-written clients guarding responses. A client that assumes every 200 contains valid JSON turns somebody else's outage into a crash in your process.