Most API test suites are decorative.
They're green, they run on every build, and they check almost nothing that matters: a wall of status is 200 assertions over happy-path requests. When the API starts returning the wrong data, the wrong errors, or leaking internals in a stack trace, the suite stays green — because nobody ever decided what it was supposed to catch.
Tools execute tests; testers choose them. Given an endpoint, what should you actually verify? Here's the framework I use — five layers, three axes of case design, and a way to prioritise when you can't test everything first. Examples use Postman's test scripts, but the framework is tool-agnostic.
The layered checklist
For every endpoint that matters, work outside-in through five layers.
1. Protocol layer
The right status code for the right situation — and specific failures, not generic ones: 400 for malformed input, 401 for missing credentials, 403 for insufficient rights, 404 for absent resources. Plus the correct Content-Type, and a sensible response time.
pm.test("Status is 200", () => pm.response.to.have.status(200));
pm.test("Content-Type is JSON", () => {
pm.expect(pm.response.headers.get("Content-Type"))
.to.include("application/json");
});
pm.test("Responds within 500 ms", () => {
pm.expect(pm.response.responseTime).to.be.below(500);
});
Cheap to write, catches an entire class of regressions. But if your suite stops here, it's decorative.
2. Contract layer
The response structure matches its schema: required fields present, types correct, formats valid. This is what protects the API's consumers — a renamed field or a number that quietly became a string breaks every client, while your value assertions may not even notice.
Postman ships the ajv JSON-schema validator, so contract testing is ten lines:
const userSchema = {
type: "object",
required: ["id", "name", "email"],
properties: {
id: { type: "integer" },
name: { type: "string" },
email: { type: "string", pattern: "@" }
}
};
pm.test("Response matches the user schema", () => {
pm.expect(pm.response.json()).to.be.jsonSchema(userSchema);
});
3. Data layer
The values are right: the created user has the name you sent; the filtered list contains only matching records; the total equals the sum of the parts; page 2 is actually different from page 1.
const body = pm.response.json();
pm.test("Filter returns only active users", () => {
body.forEach(user => pm.expect(user.status).to.eql("active"));
});
4. Behaviour layer
State actually changed. After DELETE, a follow-up GET returns 404 — deletion that only claims to work is a classic bug. After PATCH, only the patched fields differ. POSTing the same order twice doesn't charge twice.
These require chaining requests — capture an ID from one response and use it in the next:
// In the POST's post-response script:
pm.test("User created", () => pm.response.to.have.status(201));
pm.collectionVariables.set("newUserId", pm.response.json().id);
// Next request: GET {{baseUrl}}/users/{{newUserId}}
// Then: DELETE, then GET again expecting 404
The behaviour layer is where the most expensive bugs live, and it's the layer happy-path suites never reach.
5. Security layer
Every negative auth case: no token (expect 401), an expired token (401), a valid token for the wrong user (403), a valid token with insufficient role (403). Plus: error responses that don't leak stack traces or internal hostnames.
Authentication isn't plumbing to get through on the way to your tests. It is a test surface — usually the highest-risk one you have.
Positive, negative, and boundary
For each input an endpoint accepts, generate cases along three axes:
Positive — valid, typical input succeeds. The case everyone writes.
Negative — invalid input fails correctly: missing required fields, wrong types, malformed JSON, unknown IDs, illegal state transitions. The assertion is not merely "it fails" but "it fails with the right status and a useful error body." An API that returns 500 for bad input has a bug even though it rejected the request — the server failed to validate.
Boundary — the edges: empty strings, zero, negative numbers, maximum lengths, page sizes of 0 and 1 and the documented limit and the limit plus one, Unicode in text fields.
A practical heuristic: for a typical endpoint, expect one or two positive cases and five to ten negative and boundary cases. If your suite is mostly green-path, it is mostly decorative.
In Postman, the clean way to industrialise this is a data file. One request, one parameterised test, a CSV of cases:
email,password,expectedStatus
valid@example.com,correct-pass,200
valid@example.com,wrong-pass,401
,correct-pass,400
not-an-email,correct-pass,400
pm.test("Status matches the data file", () => {
pm.response.to.have.status(
Number(pm.iterationData.get("expectedStatus"))
);
});
Adding a case is now a spreadsheet edit — which means anyone on the team can extend coverage, not just the people who write JavaScript.
Prioritising: you cannot test everything first
Order the work by risk:
- Endpoints whose failure costs most — auth, payment, anything that writes data
- Endpoints that change most often — change is where regressions come from
- Everything else
A ten-test suite on the login endpoint beats a hundred tests on a static reference lookup.
And adopt the single highest-value habit in test automation — the regression contract: every bug found in production earns a permanent test reproducing it. Over a year, this builds the most valuable suite you'll ever own, because it is provably aligned with how your system actually fails.
Keeping the suite honest
Three disciplines keep a growing suite trustworthy:
Deterministic tests. A test that sometimes fails without a code change is worse than no test — it trains the team to ignore red. Hunt flakiness down; it's usually shared state, missing teardown, or time-dependent assertions.
Independent tests where possible. Chained workflows are necessary, but keep chains short and self-contained. A suite where request 40 depends on request 3 is unmaintainable.
Readable failures. Name tests so a red line states the defect. "PATCH ignores read-only fields" tells the developer everything before they even open the tool. "Test 12 failed" tells them nothing.
The one-line summary
A test suite earns its keep not by the number of green ticks but by the decisions behind them: five layers per endpoint, five-to-one negative-to-positive ratio, risk-ordered, deterministic, and readable when red.
This article is adapted from Chapter 8 of my free, open-source book *API Testing Using Postman: The Practical Guide to Modern API Testing** — 15 chapters from your first request to AI-assisted CI/CD pipelines, with a 37-question interview FAQ. Read it online, grab the PDF/EPUB, or contribute on GitHub.*
I'm a PhD researcher in Computer Science at Nottingham Trent University working on cybersecurity and AI-assisted security testing. More at imranalmunyeem.com.
Top comments (0)