The first API collection I inherited was almost entirely happy path: valid request in, 200 out, assert the fields. Real traffic does not behave like that, which is why testing API error handling deserves its own dedicated pass. Users send garbage, tokens expire mid-session, networks drop, and third-party services go down. If you only test success, you are only testing the version of your product that exists on a good day.
Map your 4xx and 5xx responses before automating anything
For each endpoint, I write down what should happen for a missing resource, a validation failure, a forbidden action, and a server fault. If nobody on the team can answer, that is already a finding. The gaps I hit most often in Postman:
- 404 vs 500: requesting
/orders/999999should return a clean 404, not a 500 from an unhandled null. - 401 vs 403: "not logged in" and "logged in but not allowed" are different bugs with different fixes, so I test them as separate cases.
- Silent 200s: endpoints that return 200 with an error buried in the body, which breaks every client that trusts status codes.
Malformed payloads and broken auth
I keep a small set of hostile requests for every write endpoint: truncated JSON, wrong types, negative numbers, empty bodies, unexpected extra fields. For example:
POST /api/orders
Content-Type: application/json
{"items": "not-an-array", "qty": -3
That request is broken twice, invalid JSON and a negative quantity, and it should produce a 400 with a clear validation error, never a 500. For auth I always cover three states: no token, an expired token, and a valid token that belongs to a different user. An expired token should return a 401 with a machine-readable code so the client can refresh silently instead of dumping the user back to the login screen.
Rate limits, timeouts, and retries
Hitting a rate limit should produce a 429, ideally with a Retry-After header, and the client should back off instead of hammering the server harder. Timeouts matter just as much: I test what the app does when a response takes far longer than expected, because a spinner that never resolves is a bug even if the API eventually answers. And before anyone adds automatic retries, I check whether the endpoint is idempotent. Retrying a flaky search is free; retrying a flaky payment can charge someone twice.
Error quality: where API error handling meets the user
A good error response is boring and predictable:
HTTP/1.1 400 Bad Request
{
"code": "VALIDATION_ERROR",
"message": "qty must be a positive integer",
"field": "qty"
}
I check three things every time. First, no stack traces, SQL fragments, or internal hostnames ever reach the client; those belong in server logs, and leaking them is a security problem as much as a quality one. Second, the code is stable enough for the frontend to branch on. Third, the message is safe to show a human, or the frontend keeps its own copy keyed off the code.
That last point is really a contract between frontend and backend, and it deserves regression coverage like any other API surface. On a recent project, a backend rename of one error code string turned a failed login into a blank screen, because the mobile app matched on the old value. Since then, the agreed error shapes live in a shared collection that runs in CI next to our smoke suite, so a renamed code fails a build instead of failing a user.