How to Write API Test Cases: Template + Examples (2026)
Quick answer
A well-written API test case has five parts: an ID and title that name exactly what's being tested, a precondition (what state the system must be in), the request (method, endpoint, payload), the expected result (status code and response shape, not just "it works"), and a postcondition confirming no unintended side effects. Writing test cases in this template before automating them keeps coverage intentional — positive, negative, and boundary cases planned on purpose, not discovered by accident when something breaks in production.
Reviewed by Rishi Gaurav
An API test case is a documented, specific scenario describing exactly what to send to an endpoint and exactly what should come back — distinct from the test automation code that later executes it. Writing the test case first, even in outline form, forces a decision about what "correct" means before you write the assertion that checks it, and produces something a non-engineer can review before any code exists.
This guide gives a reusable template, worked examples across positive, negative, boundary, and security scenarios, a naming convention, and how each written test case maps directly onto an automated test in whichever framework you use.
In this guide
- The API Test Case Template
- Naming Convention
- Positive Test Case Examples
- Negative Test Case Examples
- Boundary Test Case Examples
- Security Test Case Examples
- How Many Test Cases Does One Endpoint Need?
- From Test Case to Automated Test
- Common Mistakes When Writing Test Cases
- Deciding which cases to write first
- Parameterise instead of copying
- Writing cases someone else can review
- When to delete a test case
- Frequently asked questions about API test cases
The API Test Case Template
| Field | Description |
|---|---|
| ID | A unique, sortable identifier (e.g. TC_Users_Get_001) |
| Title | A one-line description of exactly what's being tested |
| Precondition | What state the system must be in before this test runs |
| Method & Endpoint | The HTTP method and path under test |
| Request Payload | Headers, query params, and body sent with the request |
| Expected Status Code | The exact status code expected — not "success," a number |
| Expected Response | The response shape or specific field values expected |
| Postcondition | What should (or should not) have changed as a result |
Every field earns its place: skipping Precondition produces a flaky test that only passes when run in a specific order; skipping Postcondition misses side effects a status-code-only check can't catch (a DELETE that returns 200 but doesn't actually delete anything).
If you need a target to practise against before pointing this at your own service, public and dummy APIs for testing lists the stable, key-free sandboxes worth using.
If you are on the other side of this — being asked about it rather than doing it — API testing interview questions covers what each round is actually assessing.
Naming Convention
A consistent pattern makes a test report scannable without opening each case individually:
TC_[Feature]_[Action]_[Condition]_[ExpectedResult]
Examples:
TC_Users_Get_ValidID_Returns200
TC_Users_Get_NonexistentID_Returns404
TC_Users_Create_MissingEmail_Returns400
TC_Users_Create_DuplicateEmail_Returns409
TC_Posts_Delete_UnauthorizedUser_Returns403
The exact format matters less than consistency across the whole suite — pick one, document it once, and every test case's purpose is readable from its name alone in a CI report.
Positive Test Case Examples
| ID | Title | Precondition | Request | Expected |
|---|---|---|---|---|
| TC_Users_Get_001 | Get an existing user by valid ID | User with ID 1 exists | GET /users/1 | 200, body matches user schema |
| TC_Posts_Create_001 | Create a post with valid required fields | User with ID 1 exists | POST /posts with {title, body, userId: 1} | 201, response echoes submitted fields plus a new id |
| TC_Posts_Update_001 | Update an existing post's title | Post with ID 1 exists | PUT /posts/1 with updated title | 200, title reflects the new value |
Negative Test Case Examples
| ID | Title | Precondition | Request | Expected |
|---|---|---|---|---|
| TC_Users_Get_002 | Get a user with a nonexistent ID | User with ID 999999 does not exist | GET /users/999999 | 404, error body has consistent shape |
| TC_Posts_Create_002 | Create a post missing the required title field | — | POST /posts with {body, userId: 1} (no title) | 400/422, error message names the missing field |
| TC_Posts_Create_003 | Create a post with a wrong data type | — | POST /posts with userId: "one" (string, not integer) | 400/422, rejected before reaching business logic |
Boundary Test Case Examples
Bugs cluster at the edges of valid ranges far more often than in the middle, which is why boundaries deserve their own dedicated cases rather than being left to chance:
| ID | Title | Precondition | Request | Expected |
|---|---|---|---|---|
| TC_Posts_List_001 | Request page 0 of a paginated list | — | GET /posts?_page=0 | Defined behavior (either treated as page 1, or a 400) — whichever the spec documents, tested explicitly |
| TC_Posts_List_002 | Request a page number past the last page | Only 10 pages exist | GET /posts?_page=999 | 200 with an empty result array, not an error |
| TC_Posts_Create_004 | Submit a title at the maximum allowed length | Max length is documented as 255 chars | POST /posts with a 255-char title | 201, accepted at exactly the limit |
| TC_Posts_Create_005 | Submit a title one character over the maximum | — | POST /posts with a 256-char title | 400/422, rejected just past the limit |
Ready to shift left with your API testing?
Try our no-code API test automation platform free. Generate tests from OpenAPI, run in CI/CD, and scale quality.
Security Test Case Examples
| ID | Title | Precondition | Request | Expected |
|---|---|---|---|---|
| TC_Users_Get_003 | Access a resource without authentication | — | GET /account with no Authorization header | 401 |
| TC_Users_Get_004 | Access another user's private resource (BOLA check) | User A is authenticated; resource belongs to User B | GET /users/{userB_id}/private-data as User A | 403 (not 200 with User B's data) |
| TC_Auth_Login_001 | Attempt login with an expired token on a protected route | Token issued more than its TTL ago | GET /account with an expired Authorization header | 401, not a degraded but authenticated response |
See our full checklist for the complete set of functional, security, and performance items these examples are drawn from, and the OWASP API Top 10 testing guide for deeper security-specific cases like these.
How Many Test Cases Does One Endpoint Need?
Enough to cover the happy path, every documented error status, key boundary values per input field, and at least one authorization case. For a typical CRUD endpoint, that's commonly 8–15 test cases:
- 1–2 positive cases (valid input, valid ID)
- 2–4 negative cases (missing fields, wrong types, invalid IDs)
- 2–3 boundary cases (min/max length, empty, pagination edges)
- 1–2 security cases (unauthenticated, unauthorized/BOLA)
- 1–2 idempotency or side-effect cases (retry, verify postcondition)
Endpoints with more complex business logic — conditional validation, multi-step workflows, state machines — need proportionally more.
From Test Case to Automated Test
Each row in the tables above maps directly onto one automated assertion. TC_Users_Get_002 (nonexistent ID returns 404) in a few frameworks:
# pytest
def test_get_user_nonexistent_id_returns_404(api_session, base_url):
response = api_session.get(f"{base_url}/users/999999", timeout=10)
assert response.status_code == 404
// REST Assured
@Test
void getUserNonexistentIdReturns404() {
given()
.when()
.get("/users/999999")
.then()
.statusCode(404);
}
# Karate
Scenario: get user with nonexistent ID returns 404
Given path 'users/999999'
When method get
Then status 404
See the pytest, REST Assured, Playwright, Cypress, and Karate tutorials for the complete pattern in each framework — every one of them accepts test cases written in this template as direct input.
Common Mistakes When Writing Test Cases
- Writing "it works" as the expected result instead of the exact status code and response shape — an ambiguous expectation produces an ambiguous automated assertion.
- Skipping the precondition, producing a test that only passes when run after another specific test happens to have created the data it needs.
- Testing only the happy path per endpoint and treating negative/boundary cases as optional extras rather than an equal third of the suite.
- No postcondition check, so a
DELETEthat returns200without actually deleting anything passes undetected. - Inconsistent naming across the suite, making a CI failure report unreadable without opening every individual case.
- Writing test cases that describe implementation instead of behavior — "calls the UserService" instead of "returns 200 with the user's email field," which breaks the moment the implementation is refactored even though behavior is unchanged.
Deciding which cases to write first
An endpoint can support dozens of defensible test cases and rarely deserves all of them. Ranking by risk rather than by category keeps the suite proportional to what it protects.
| Signal | Why it raises priority |
|---|---|
| Money or entitlement changes hands | A wrong result is not recoverable by a retry |
| The endpoint writes or deletes | Read failures inconvenience; write failures corrupt |
| It enforces an authorization boundary | The failure is a data breach, not a bug report |
| It has broken before | Past defects cluster; the code is complex for a reason |
| It has many callers | Blast radius, and slower detection |
| The logic branches on input | Branches are where untested paths hide |
Work down that list and stop when the marginal case stops encoding a requirement anyone would be paged for. A practical shape for a typical write endpoint is one happy path, two or three validation failures on the fields that matter, one authorization case per role that should be refused, and a boundary case wherever a limit exists. That is six to eight cases, not thirty.
The corollary matters as much: a read-only endpoint returning public reference data can reasonably have one test. Spending equal effort per endpoint is how suites become slow without becoming safer.
Free Guided worksheet
Build Your Testing Strategy in 30 Minutes
A structured worksheet that walks you through defining your testing strategy in 30 minutes. Cover architecture, tools, layers, and team responsibilities.
Download FreeParameterise instead of copying
Once you have identified the boundaries, writing one case per value produces near-identical blocks that drift apart as they are edited. Table-driven cases keep the intent in one place:
import pytest
@pytest.mark.parametrize("qty,expected_status,reason", [
(1, 201, "minimum valid quantity"),
(99, 201, "maximum valid quantity"),
(0, 422, "below minimum"),
(100, 422, "above maximum"),
(-1, 422, "negative"),
("two", 422, "wrong type"),
])
def test_order_quantity_boundaries(client, qty, expected_status, reason):
response = client.post("/v1/orders", json={"sku": "A-1", "qty": qty})
assert response.status_code == expected_status, f"{reason}: got {response.text}"
Two things this buys. Adding a boundary is one line rather than one function, so people actually add them. And the failure message names the reason — "above maximum: got 201" tells a reviewer what broke without opening the file.
The limit to watch: parameterisation is for cases that differ only in data. When cases differ in setup or in what they assert, separate functions stay clearer than a table with conditionals in it.
Writing cases someone else can review
A test case is read far more often than it is written — during review, during triage, and by whoever inherits it. Four habits make that reading cheap.
Name the behaviour, not the mechanics. test_order_rejected_when_quantity_below_minimum tells a reviewer what the system should do. test_post_orders_2 tells them nothing and hides whether the case duplicates another.
Make setup obvious at the top. A reviewer should see what state the test assumes without following three layers of fixtures. Shared fixtures are good; hidden ones are not.
Assert one behaviour per case. A test asserting six unrelated things fails on the first and hides the rest, and its name cannot honestly describe it.
Say why, when why is not obvious. A case that exists because of a past incident should say so in one line. Otherwise someone eventually deletes it as redundant, and the incident recurs.
When to delete a test case
Suites are pruned far less often than they are grown, and an unpruned suite loses trust slowly. Four cases are worth removing rather than fixing.
It asserts an implementation detail. A test that breaks on every safe refactor is measuring structure rather than behaviour. It costs maintenance and catches nothing.
It duplicates a case at a cheaper layer. An end-to-end test verifying a validation rule already covered by a unit test adds minutes to every run for no additional signal.
It has been skipped for more than a release. A skipped test is documentation of an intention. If nobody has returned to it, delete it and file the gap where it will actually be seen.
It cannot fail. Tests that assert something the framework or the type system already guarantees pass permanently, which reads as coverage and is not.
Everything else — including tests that fail sometimes — should be fixed rather than removed. A flaky test is reporting a real problem, usually in setup or in an assumption about ordering.
Frequently asked questions about API test cases
What are the essential parts of an API test case? An ID and title, a precondition, the request itself, the expected result (status code and response shape), and a postcondition confirming no unintended side effects.
What is the difference between positive and negative test cases? A positive case sends valid input and expects success. A negative case deliberately sends invalid input and expects the API to reject it correctly.
What are boundary test cases in API testing? Cases targeting the edges of valid input ranges — minimums, maximums, empty values, and pagination edges — where bugs cluster more often than in the middle of a valid range.
How do I name API test cases consistently?
A common pattern is TC_[Feature]_[Action]_[Condition]_[ExpectedResult]. The exact format matters less than applying it consistently across the suite.
How many test cases does one API endpoint need? Commonly 8–15 for a typical CRUD endpoint: the happy path, documented error statuses, boundary values, and at least one authorization case.
Should I write test cases before or after building the automated tests? Before, at least in outline — it forces a decision about what "correct" means before you write the assertion, and produces a reviewable artifact.
Sources and further reading
- RFC 9110 — HTTP Semantics — the normative definition of methods, status codes and headers.
- OpenAPI Specification — the normative spec for describing HTTP APIs.
- ISTQB Glossary — the standard vocabulary for test terms.
Key takeaways
- Precondition and postcondition are the most commonly skipped template fields — and the ones that catch flaky ordering bugs and silent side effects.
- "It works" is not an expected result. Write the exact status code and response shape.
- Positive, negative, and boundary cases are roughly equal thirds of a complete suite, not happy-path-plus-afterthoughts.
- A consistent naming convention makes CI failure reports scannable without opening every case.
- 8–15 test cases per typical CRUD endpoint is a reasonable baseline; complex business logic needs more.
- Every written test case maps directly onto one automated assertion, regardless of which framework executes it.
Generate Test Cases Automatically from Your OpenAPI Spec
Writing 8-15 test cases per endpoint by hand scales linearly with your API's size. Total Shift Left generates positive, negative, and boundary test cases directly from your OpenAPI spec — the same structure as the template above, covering every endpoint automatically and regenerating when the spec changes.
Start your free trial to see generated test cases for your own API, or see plans and pricing if you're already evaluating.
Ready to shift left with your API testing?
Try our no-code API test automation platform free.