DEV Community

Cover image for How to Design an API Test Strategy (Not Just Accumulate Tests)
Imran Al Munyeem
Imran Al Munyeem

Posted on • Originally published at imranalmunyeem.github.io

How to Design an API Test Strategy (Not Just Accumulate Tests)

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);
});
Enter fullscreen mode Exit fullscreen mode

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);
});
Enter fullscreen mode Exit fullscreen mode

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"));
});
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode
pm.test("Status matches the data file", () => {
    pm.response.to.have.status(
        Number(pm.iterationData.get("expectedStatus"))
    );
});
Enter fullscreen mode Exit fullscreen mode

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:

  1. Endpoints whose failure costs most — auth, payment, anything that writes data
  2. Endpoints that change most often — change is where regressions come from
  3. 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)