Build the mental model
A status code of 200 tells you the HTTP layer worked: the server received your request and responded successfully at the protocol level. It tells you nothing about whether the JSON body inside actually contains the fields your code expects, in the types it expects.
This is the gap response validation closes, and it is easy to overlook while building against a provider's current, well-behaved API.
- A field gets renamed
- A number silently becomes a string
- A value that used to always exist starts returning null
- A new required field appears inside a nested object
If your code reads `response.data.customer.email` blindly and any link in that chain is missing or the wrong shape, you get a runtime crash, or worse, silently wrong behavior that surfaces later as corrupted data downstream.
The core discipline
Check that every field your code will use exists and has the matching type before it goes further into the application. When a check fails, fail safely and loudly instead of silently propagating malformed data.
RESPONSE VALIDATION DECISION FLOW
---------------------------------
Response arrives (status 200)
|
v
Check required fields exist
Check each field's type matches
|
v
Valid? ------ NO ---> Fail safely: clear error,
| do not use the data
YES
|
v
Use the validated data
in the rest of your appConnect it to a real scenario
`validateResponse` takes a parsed response object and a small shape description mapping each required field name to its expected JavaScript type. It walks the shape, checking each field is present, is not null, and matches the type, collecting every problem rather than stopping at the first.
The valid example is a charge response with a nested customer object matching the shape exactly, so the function returns `{ valid: true, data: ... }`, safe to use as-is.
The malformed example simulates a real provider regression: `amount` arrived as a string, `status` arrived as null, and `customer` is missing entirely. The function does not throw or crash; it returns `{ valid: false, errors: [...] }` listing all three problems in one pass.
Try the working example
const expectedShape = {
id: "string",
amount: "number",
status: "string",
customer: "object"
};
function validateResponse(response, shape) {
const errors = [];
for (const [field, type] of Object.entries(shape)) {
if (!(field in response)) {
errors.push(`missing field: ${field}`);
continue;
}
const value = response[field];
if (value === null) {
errors.push(`field "${field}" is null, expected ${type}`);
continue;
}
const actualType = Array.isArray(value) ? "array" : typeof value;
if (actualType !== type) {
errors.push(`field "${field}" is ${actualType}, expected ${type}`);
}
}
return errors.length === 0 ? { valid: true, data: response } : { valid: false, errors };
}
const validResponse = { id: "ch_123", amount: 5000, status: "succeeded", customer: { id: "cus_1" } };
const malformedResponse = { id: "ch_124", amount: "5000", status: null };
console.log(validateResponse(validResponse, expectedShape));
console.log(validateResponse(malformedResponse, expectedShape));validResponse -> { valid: true, data: { id: 'ch_123', amount: 5000, status: 'succeeded', customer: { id: 'cus_1' } } }
malformedResponse -> { valid: false, errors: [
'field "amount" is string, expected number',
'field "status" is null, expected string',
'missing field: customer'
] }5-minute try-it
Add a new field, `currency: "string"`, to `expectedShape`. Run it against `validResponse` without adding a `currency` field to that object, and see how the error output changes.
One important caution
Accessing a deeply nested field like `response.data.customer.email` without checking each link in the chain
Skipping validation entirely because you assume the provider's API will never change
typeof - MDN — API Integration & Webhooks