
REST API Test Cases to Write for Every Endpoint
Short answer: for each REST endpoint, write test cases in five groups: the happy path and its response contract, input validation and boundaries, authentication and authorization, error codes for missing resources, and pagination or idempotency where the endpoint has them. Each case is one request with a few assertions on status, body and headers. The fast, deterministic ones run on every pull request.
This checklist walks through the five groups, shows how to write each case as an assertion in a YAML flow, and ends with which cases belong in CI on every pull request.
How to pick test cases for an endpoint
A good case checks one behaviour and can fail on its own. If a single test checks the status, the auth rule and the pagination, a red result does not tell you which of the three broke. The groups below are ordered by how much each one catches per minute of writing: the happy path first, then the inputs and permissions that clients get wrong most often.
This list stays on one endpoint at a time, so you can copy it per route. For a broader regression list across a whole API, see the API regression testing checklist.
1. Happy path and response contract
Start with one valid request and assert what a client depends on. Three checks cover most of it:
- Status. The code is the one the endpoint documents, such as 200 for a read or 201 for a create. DevTools assertions are expressions on the response, for example
response.status == 200, orresponse.status >= 200 && response.status < 300for a range. - Body fields. The fields clients read are present and have the right values, such as
response.body.id != null. - Headers. The
Content-Typeis what the client expects. Header assertions readresponse.headers.
Assert a handful of stable fields instead of the whole body. Timestamps and generated ids change on every run and make the case flaky. The deterministic assertions guide covers that trade-off.
2. Input validation and boundary cases
Send the request the endpoint should reject, and assert the rejection. Write one case per rule, not one case that breaks everything at once:
- A required field is missing.
- A field has the wrong type, such as a string where a number belongs.
- A string is empty, or longer than the documented limit.
- A number sits exactly at, one below and one above its minimum or maximum.
The status depends on your API's contract. You might answer 400, or 422, which RFC 9110 defines for well-formed content that the server cannot process. Pick one convention, assert it in every case, and also assert that the response names the offending field so a client can show it. A validation case that only checks for "some 4xx" will pass when the server fails for the wrong reason.
3. Authentication and authorization
These are three separate cases, and they should return different codes:
- No credentials. The request carries no token. RFC 9110 describes 401 as a request that "lacks valid authentication credentials for the target resource".
- Invalid credentials. An expired or malformed token also gets 401.
- Valid credentials, wrong permission. A user without access to the resource gets 403, where the server "understood the request but refuses to fulfill it".
Keep these apart in your assertions. A test that accepts "401 or 403" hides the bug where a missing token returns 403, or where a forbidden user returns 401. Put the token in an environment variable so the same case runs against staging and local. DevTools assertions can reference environment variables, as in response.status == {{expected_status}}.
4. Error codes and missing resources
Ask for something that does not exist and assert the error body as well as the status. The usual case is a read by an id that was never created, which should return 404. RFC 9110 says a 404 means the server found no current representation for the target resource, or is not willing to disclose that one exists. Because of that second clause, an API can deliberately answer 404 for a record the caller may not see.
Then check two more things. A method the route does not support should fail with a clear status, not a 500. And every error response should share one body shape, so clients parse failures in one place. Assert that shape on at least one 4xx case per endpoint.
5. Pagination and idempotency
Skip this group for an endpoint that returns one record or changes nothing. For list and write endpoints, it catches bugs the first four miss.
Pagination. Request a page smaller than the total and assert the number of items returned. Then request the last page and a page past the end. The last page should return the remainder, and the page past the end should return an empty list or the error your contract defines. Also assert that a page size above the maximum is clamped or rejected.
Idempotency. An HTTP method is idempotent when repeating the same request has the same intended effect as sending it once. PUT and DELETE are idempotent by definition in the HTTP semantics specification. So send the same PUT twice and assert that both return success and leave the same state. Send the same DELETE twice and assert the second call follows your contract, which is often 404 or 204. POST is not on that list, so assert what happens when a client retries one: a duplicate record, or a rejection if you support idempotency keys.
Write each case as an assertion in a YAML flow
In DevTools, as the flow docs describe assertions, a request step can carry an assertions list, and each entry is an expression. Here are three cases from the groups above for one endpoint:
- request:
name: GetUserHappyPath
method: GET
url: '{{BASE_URL}}/users/1'
headers:
Authorization: 'Bearer {{API_TOKEN}}'
assertions:
- response.status == 200
- response.body.id == 1
- response.body.email != null
- request:
name: GetUserNoToken
method: GET
url: '{{BASE_URL}}/users/1'
assertions:
- response.status == 401
- request:
name: GetUserMissing
method: GET
url: '{{BASE_URL}}/users/999999'
headers:
Authorization: 'Bearer {{API_TOKEN}}'
assertions:
- response.status == 404
For a case that depends on an earlier response, such as create then fetch, give the first step a name and reference it in the next with depends_on. The API chain testing guide shows the pattern for REST. When an expression is not enough, a JavaScript node can handle complex validations.
Avoid random values in assertions. The DevTools docs warn that faker values are not deterministic and should not be used for golden-output assertions. For more assertion shapes, see API assertions in YAML and JSON assertion patterns.
Which cases belong in CI on every pull request
Run the cases that are fast and deterministic on every pull request: the happy path, validation, auth and error-code cases for each endpoint you changed or depend on. They need no wait time and no shared state, so a failure means a real regression. Leave slower cases, such as large pagination sweeps or long idempotency sequences, for a merge or nightly run.
The DevTools docs show a GitHub Actions workflow that triggers on pull requests to main. The job runs the flow file with devtools flow run api-tests.yaml --report junit:test-results.xml. The CLI exits 0 when all flows succeed and 1 when one or more fail, so a failed assertion fails the job. For the workflow in full, see API testing in GitHub Actions, and for reading failures in the pull request, the JUnit reports guide.
Frequently asked questions
How many test cases does one REST endpoint need?
There is no fixed number. Start with one case per group that applies: a happy path, the validation rules that matter, the three auth cases and one error case. Add pagination or idempotency cases only where the endpoint lists records or accepts repeated writes.
Should I test every status code an endpoint can return?
Test every status your contract documents and your clients handle. A code that no client branches on and that the server rarely returns can wait until it causes a bug.
Should PUT and DELETE tests run twice?
Yes, once to check the first effect and once to check the repeat. The specification defines both methods as idempotent, so a repeated call should leave the same state.
Conclusion
Per endpoint, write the happy path, the inputs it should reject, the three auth cases, a missing resource, and pagination or idempotency where they apply. Express each as a request with a short assertions list, commit the YAML next to the code, and run the fast cases on every pull request so the exit code decides the build. Start with the endpoint that changes most often and copy the pattern to the rest.
