REST API test cases: a checklist with examples
An API has no form in front of it to stop bad input, so you test it with requests a polite client would never send, and you read the whole response: the status code and the headers count as much as the body. This checklist works through one small endpoint for booking meeting rooms, but most of it carries over to any JSON API.
The example endpoint
POST /bookings takes {"room", "date", "start", "end", "people"} and creates
a booking. Its documented rules: people is a whole number from 1 to 10, the date is from
today up to 60 days ahead, times are on the hour or half past between 09:00 and 18:00, and bookings of
one room must not overlap. Every request carries a bearer token, and each user sees only their own
bookings. Swap in the numbers from your own API's reference.
Status codes and headers on the happy path
Start with requests that should work, and check more than "it returned 2xx".
| Request | Expected | What to look at |
|---|---|---|
POST /bookings, valid body | 201 Created | A Location header; then send a GET to that exact URL and expect 200 |
GET /bookings/{id} | 200 | The fields you sent come back unchanged, plus the server's own (id, owner, created_at) |
PATCH /bookings/{id}, one field | 200 | Only that field changed |
DELETE /bookings/{id} | 204 | An empty body; a GET afterwards gets 404 |
| The same DELETE again | Usually 404 | Whatever the reference says, and the same answer as for an id that never existed |
PUT /bookings (a method the endpoint lacks) | 405 | An Allow header that lists the methods that do work |
Following the Location header is worth the extra request. A typo in how the server builds
that URL (a singular instead of a plural, a missing prefix) passes every test that only reads the body.
Validation boundaries
Each rule has values just inside and just outside it. Expect the inside value to be accepted and the outside one to get a 4xx with an error that names the field.
| Field | Values to send | Expected |
|---|---|---|
| people | 1 and 10 | Accepted |
| people | 0, 11, -1 | Rejected |
| people | "3" (a string), 2.5, null, the field left out | Rejected, unless the reference says strings are converted |
| date | Today and today + 60 days | Accepted |
| date | Yesterday, today + 61 days | Rejected |
| date | A day that does not exist inside the window, such as November 31 | Rejected: the format is right, the day is not |
| start / end | 09:00–10:00, 17:00–18:00 | Accepted |
| start / end | 08:30 start, 18:30 end, 10:15 start | Rejected |
| start / end | end equal to start, end before start | Rejected |
Opening hours have two edges. A server that checks only the start against closing time will take a
booking that starts at 17:30 and ends at 19:30, so send one that starts inside the hours and ends
outside them. Impossible dates need care too: a regular expression for YYYY-MM-DD
accepts November 31, and only a real calendar check rejects it. Pick the impossible date inside the
60-day window, or the server may reject it as too far ahead and the calendar check never runs.
Broken and unexpected requests
- Invalid JSON, such as
{"room": r1}: expect 400 and a JSON error. A 500, or a stack trace in plain text, is a bug even though the request was wrong, and the trace tells strangers about your code. - An empty body, a JSON array instead of an object, a body sent without
Content-Type: application/json. - Fields the reference does not mention: usually ignored, never stored.
- Fields the server owns, such as
id,ownerorcreated_at, in a POST or PATCH body. If the server copies the whole body into the record, a user can rename a booking's owner or give it someone else's id. This is often called mass assignment, and a PATCH with one extra field is enough to find it.
Overlapping bookings
Business rules that compare a new record with existing ones break in more ways than single-field checks. Take an existing booking for room r1 from 10:00 to 11:00 and try these, each in the same room and on the same date:
| New booking | Expected | Why it is worth a test |
|---|---|---|
| 09:00–10:00 | Accepted | Back to back, ends when the other starts |
| 11:00–12:00 | Accepted | Back to back, starts when the other ends |
| 10:30–11:30 | 409 Conflict | Starts inside the existing booking |
| 09:30–10:30 | 409 Conflict | Starts before it and runs into it |
| 10:00–10:30 | 409 Conflict | Lies inside it |
| 09:00–12:00 | 409 Conflict | Covers it completely |
| 10:00–11:00 in another room | Accepted | The check must be per room |
Two bookings overlap when the new start is before the old end and the new end is after the old start.
Writing <= instead of < rejects back-to-back bookings. Checking only
whether the new start falls inside an old booking misses the "starts before" and "covers" rows, which
is why the table has both.
Then repeat the rule on the other paths. A PATCH that moves a booking onto a taken slot should get the same 409, and a PATCH that only extends a booking into its own time should not conflict with itself. After a DELETE, the freed slot should be bookable again at once; a server that marks bookings as deleted but forgets to skip them in the overlap check will keep the slot blocked. If there is a limit per user, such as three bookings a day, make the third and then the fourth.
Two users, one API
You need two accounts or tokens for this part. Create a booking as user A and note its id, then switch to user B's token:
GET /bookings/{A's id}as B: 404 (or 403, if the reference says so), never A's data.PATCHandDELETEon A's booking as B: refused. After a refused DELETE, check as A that the booking is still there; the status code alone does not prove nothing happened.GET /bookingsas B: none of A's bookings, and atotalthat counts only B's.- No
Authorizationheader, a token with one character changed, the word Bearer left out: 401 every time.
A server that checks that you are logged in, but not that the record is yours, has what the OWASP API Security Top 10 calls broken object level authorization, first on its list. Try it on every endpoint that takes an id, because each one runs its own query and can miss the owner check on its own.
Lists and pagination
Create five bookings and walk the list with a small page size, such as
?limit=2&page=1, then pages 2 and 3. Every booking should appear exactly once across
the pages, in the documented order, and the last page should hold one item. An off-by-one in the
offset shows up as an item repeated at the start of page 2 or skipped between pages. Also send
page=0, a limit one above the documented maximum, and a filter that matches nothing
(expect an empty list with a total of 0, not a 404).
Parallel requests
A server that first checks for a clash and then inserts can let two requests through if they arrive together. Send the same booking twice at once:
DATE=2026-12-01 # any date inside the allowed window
for i in 1 2; do
curl -s -o /dev/null -w "%{http_code}\n" -X POST https://api.example.com/bookings \
-H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
-d "{\"room\":\"r1\",\"date\":\"$DATE\",\"start\":\"14:00\",\"end\":\"15:00\",\"people\":3}" &
done; wait
Expected: one 201 and one 409. Two 201s mean a double booking. Run it a few times, since a race does not show up on every attempt. The double submit guide covers the same problem from the side of a form.
Open the practice API
More guides: double submit · what the server trusts · all guides