Test cases for a REST API endpoint (CRUD)
Updated 2026-11-13 · 7 min read
The API version of 'test a login page'. These 35 cases cover POST, GET, PUT, PATCH and DELETE with status codes, validation, idempotency, authorisation, performance and contract checks.
Assumes a JSON API for books at /api/v1/books that needs a bearer token. A book has a server-generated id, title (required, 1 to 120 characters), author (required), price (number, 0 or more) and isbn (unique). The status codes below follow one common convention; where your API documents a different one, test against the documentation.
How to answer in an interview
Ask first: is there an API specification (OpenAPI), how does authentication work, what are the validation rules, which status codes does the team use for create, delete and validation errors, are there roles, and does the API support pagination, conditional requests (ETag) or idempotency keys? Then go method by method (POST, GET, PUT, PATCH, DELETE), and finish with cross-cutting checks: headers, security, performance and contract. Say you would check the database or a follow-up GET, not just the status code.
Scope and test data
Use a test environment with two test users, [email protected] (role editor) and [email protected] (role viewer), each with their own token. Example book: title "Testing Notes", author "A. Tester", price 499, ISBN from a fake range agreed with the team. Example rules: list page size defaults to 20 with a maximum of 100. These are designed cases, not an execution report.
Create (POST)
| ID | Category | Test case | Expected result |
|---|---|---|---|
| R01 | Functional | POST a valid book | 201; body has a new id and the sent fields; Location header points to /api/v1/books/ plus the new id |
| R02 | Negative | POST without title | 400 or 422 as documented; error names the missing field |
| R03 | Boundary | POST a title of 120 characters, then 121 | 120 accepted (201); 121 rejected with a validation error |
| R04 | Negative | POST price as the string "abc" and as −1 | Validation error for each, never a 500 |
| R05 | Negative | POST a second book with an existing ISBN | 409 with a message that names the conflict |
| R06 | Negative | POST valid JSON with Content-Type: text/plain | 415 |
| R07 | Negative | POST malformed JSON (a missing closing brace) | 400; no stack trace or framework details in the body |
| R08 | Negative | POST with id or createdAt set by the client | Fields ignored or rejected as documented; server values used |
| R09 | Functional | POST the same body twice (a client retry) | Two books, or one if an idempotency key is supported and the same key was sent; documented either way |
Read (GET)
| ID | Category | Test case | Expected result |
|---|---|---|---|
| R10 | Functional | GET an existing book by id | 200; body matches what was created |
| R11 | Negative | GET an id that does not exist | 404 |
| R12 | Negative | GET with an id in the wrong format (abc for a numeric id) | 400 or 404 as documented, never a 500 |
| R13 | Functional | GET the list with no parameters | 200; at most 20 items; pagination information present |
| R14 | Boundary | GET a page beyond the last page | 200 with an empty list, or as documented; not an error |
| R15 | Functional | GET the list filtered by author and sorted by price | Only matching books, in the right order |
Update (PUT and PATCH)
| ID | Category | Test case | Expected result |
|---|---|---|---|
| R16 | Functional | PUT a full valid body to an existing book | 200 or 204; a follow-up GET shows every field changed |
| R17 | Negative | PUT a body without author | Validation error; the stored author is not silently set to null |
| R18 | Functional | PATCH only price | Only price changes; other fields unchanged |
| R19 | Negative | PUT and PATCH to an id that does not exist | 404 (or 201 for PUT if create-by-PUT is documented) |
| R20 | Functional | Send the same PUT twice | Same final state after both; no duplicate side effects |
| R21 | Negative | Update with an If-Match ETag from an older version, if ETags are supported | 412; the newer data is not overwritten |
| R22 | Negative | PUT to book 5 with "id": 6 in the body | Rejected, or the URL id wins, as documented; book 6 is untouched |
Delete (DELETE)
| ID | Category | Test case | Expected result |
|---|---|---|---|
| R23 | Functional | DELETE an existing book | 204 (or 200); a follow-up GET returns 404 |
| R24 | Functional | DELETE the same book again | 404 or 204 as documented; never a 500 |
| R25 | Negative | DELETE a book that orders still refer to | 409, or the documented cascade or soft delete; no broken references |
Methods, headers and contract
| ID | Category | Test case | Expected result |
|---|---|---|---|
| R26 | Negative | Send PATCH to the collection /api/v1/books | 405 with an Allow header listing supported methods |
| R27 | Functional | Check response headers on success and error | Content-Type is JSON on both; error bodies use one documented format |
| R28 | Functional | Validate every response against the OpenAPI schema, including errors | All responses match; no undocumented fields such as internal ids |
Security
| ID | Category | Test case | Expected result |
|---|---|---|---|
| R29 | Security | Call each method with no token and with an expired token | 401 for both; no data returned |
| R30 | Security | As Ravi, GET, PUT and DELETE a book that belongs to Asha, if books are owned per user | 403 or 404; nothing changed |
| R31 | Security | As Ravi (viewer), POST, PUT and DELETE | 403; nothing changed |
| R32 | Security | POST a title containing ' OR 1=1 -- and another containing a script tag | Stored and returned as plain text or rejected; no database error leaked |
| R33 | Security | Add "role": "admin" or another server-only field to a PATCH | Field ignored or rejected; not saved (mass assignment) |
Performance
| ID | Category | Test case | Expected result |
|---|---|---|---|
| R34 | Performance | GET by id at the expected request rate | Response times within the agreed limit; no errors |
| R35 | Performance | GET the list with limit=100000 | Capped at the maximum (100) or rejected; the server does not try to return everything |
Practise it
The free ShopLite practice app has a cart API with create, update and delete on one resource (POST /api/cart/items, then PATCH and DELETE on /api/cart/items/ plus a product id), behind a login. Adapt R02, R04, R23, R24 and R29 to it with Postman or Playwright's request API. Its conventions differ from the example above in places (its DELETE returns 200 with the updated cart rather than 204), which is the point: check each response against the app's own documentation, not against a convention from memory. The app also has deliberate bugs, so some cases should fail.
What to prioritise
R30 and R31 (authorisation), R01 and R23 with a follow-up GET (data really changed), and R04, R07 and R12 (bad input never gives a 500). The most common interview mistake is checking only the status code: say how you would check the body, the headers and the stored data too.
Downloads
Quick answers
Should a failed validation return 400 or 422?
Both are used. RFC 9110 defines 422 (Unprocessable Content) for well-formed content the server cannot process, and many APIs still return 400 for every bad request. Test that the API is consistent with its own documentation and never returns a 500 for bad input.
Is POST idempotent?
No. RFC 9110 lists PUT, DELETE and the safe methods (such as GET) as idempotent; POST is not. Sending the same POST twice may create two resources unless the API supports something like an idempotency key.
What is the most important security test for a CRUD API?
Changing the ID in the URL to another user's resource (R30). OWASP lists Broken Object Level Authorization as API1 in its 2023 API Security Top 10.
Sources
Want a project you can show in interviews?
The portfolio project kit walks you through testing a deliberately buggy app with Selenium or Playwright and API tests, then explaining it. The app itself is free.
