What REST API testing actually means
REST API testing is verifying the behaviour of an HTTP interface from the outside: you send a request, you read the response, and you assert that what came back is what the contract promised. That is deliberately narrower than unit testing. You are not checking that a function returns the right value; you are checking that the status code, headers and body an arbitrary client receives are correct, consistent and stable.
That outside-in view is what makes it valuable. An API is the one part of a system whose behaviour other
teams — and often other companies — depend on. Internal refactoring is free; changing a field name from
createdAt to created_at is not. API tests are how you find out that you broke the
contract before your consumers do.
In practice the work splits into layers. Functional tests check each endpoint does what it says. Contract tests check the response matches its published schema. Integration tests check that a sequence of calls — create, read, update, delete — works end to end. Security tests check that authentication and authorisation cannot be bypassed. Performance tests check the thing stays fast and stable under load. Most teams need all five, in roughly that order of priority.
What to test
1. Status codes
The status code is the first assertion and the one most often written too loosely. Asserting "not a 500" or
"in the 200 range" hides real bugs: a 200 where the API should have returned
201 Created means a client cannot tell creation from update, and a
200 where it should have returned 404 means clients will
cheerfully render an empty resource. Assert the exact code.
| Scenario | Expected | Common bug |
|---|---|---|
| Successful read | 200 OK | — |
| Resource created | 201 + Location | Returns 200 with no Location |
| Accepted for async processing | 202 Accepted | Returns 200, implying it is done |
| Deleted, nothing to return | 204 No Content | Returns 200 with an empty body |
| Malformed JSON or bad parameter | 400 Bad Request | Returns 500 — an unhandled parse error |
| Missing or invalid credentials | 401 + WWW-Authenticate | Returns 403, or 200 with empty data |
| Authenticated but not permitted | 403 Forbidden | Returns 404 to hide existence — defensible, but be consistent |
| Resource does not exist | 404 Not Found | Returns 200 with null |
| Method not supported here | 405 + Allow | Returns 404, sending clients hunting for a typo |
| Conflicts with current state | 409 Conflict | Returns 400, losing the distinction |
| Valid syntax, invalid content | 422 Unprocessable | Returns 200 with {"success": false} |
| Rate limit hit | 429 + Retry-After | Returns 503, or no retry hint at all |
The full status code reference covers the rest. One rule to hold onto: a
500 is always your bug. It means the server hit an unhandled condition.
A client sending nonsense should be answered with a 4xx that explains the problem, never a stack
trace.
2. Response schema and data
After the status code, assert the shape. The failure mode you are guarding against is not "the response is wrong" but "the response drifted" — a field renamed, a type quietly changed from number to string, an optional field that consumers had started depending on. Check:
- Content-Type is
application/jsonand the body actually parses as JSON. - Required fields are present, with the documented names and types.
"42"is not42, and a client written in a statically typed language will break on the difference. - Formats are valid — timestamps as ISO 8601 with an explicit offset, identifiers as the documented type, enums restricted to their documented set.
- Nulls versus omissions behave consistently. Deciding that an absent value is
nullrather than a missing key is fine; flip-flopping between the two is not. - Nothing extra leaks. Password hashes, internal IDs, and full user records nested inside an unrelated response are all common and all avoidable.
If the API publishes an OpenAPI or JSON Schema document, validate against it automatically rather than hand-writing per-field assertions. That is contract testing, and it catches the whole class of drift bugs with one assertion. When you are reading a response by hand, the JSON formatter makes structure problems obvious that are invisible in a single-line body.
3. Authentication and authorisation
These are two different tests and both matter. Authentication asks "who are you"; authorisation asks "may you do this". Every protected endpoint deserves three cases:
| Case | Send | Expect |
|---|---|---|
| Happy path | Valid token for a permitted user | 2xx |
| No credentials | No Authorization header | 401 with WWW-Authenticate |
| Bad credentials | Garbled, expired or revoked token | 401, never 500 |
| Wrong user | Valid token for user B on user A's resource | 403 or 404 |
| Wrong role | Valid token lacking the required scope | 403 |
The fourth row is the important one. Insecure direct object reference — where
GET /v1/invoices/1042 returns someone else's invoice simply because you were logged in as
somebody — is one of the most common serious API vulnerabilities, and it is trivially testable: authenticate
as one user, request another user's resource, assert you are refused. Do it for every resource-scoped
endpoint, not just the obvious ones.
Use throwaway test accounts and short-lived tokens, never production credentials. If a bearer token is being rejected and you cannot tell why, the JWT decoder will show its claims and expiry; the HTTP headers guide covers how each authentication scheme is formatted on the wire.
4. Error handling
Error responses are part of the contract and are almost always tested less than the happy path, which is
backwards — clients spend far more effort handling errors than successes. Check that the error body has a
consistent structure across the whole API, that it carries a stable machine-readable code alongside the human
message, and that validation errors identify which field failed. RFC 9457 (application/problem+json,
formerly RFC 7807) is a good default if you have no house style.
Then check what errors do not contain. Stack traces, SQL fragments, internal hostnames and framework version banners are information disclosure. And an error message that differs between "no such user" and "wrong password" hands an attacker a user enumeration oracle.
5. Edge cases
Bugs cluster at boundaries. Work through them deliberately rather than hoping:
- Empty and absent. Empty string, empty array, empty object, missing key, explicit
null— five distinct inputs that frameworks routinely conflate. - Boundaries. Zero, negative numbers, one below and one above every documented limit,
page=0,limit=0, and a page beyond the last. - Size. A field at maximum length and one character over. A payload at the body limit. A collection large enough to expose an N+1 query.
- Characters. Unicode, emoji, right-to-left text, combining accents, and strings
containing
<,&, quotes and backslashes. Percent-encoding problems in query parameters are common — the URL encoder settles whether the client or the server is at fault. - Types. A string where a number is expected, an array where an object is expected, a
deeply nested structure. All should give
400or422, never500. - Duplication. Send the same
POSTtwice. Does it create two resources? Should it? The HTTP methods guide explains why the answer depends on idempotence and what anIdempotency-Keybuys you. - Concurrency. Two simultaneous updates to one resource. With
ETagandIf-Matchyou should see 412; without them, one write silently disappears. - Ordering and pagination. Results must be deterministically ordered, or paging through them will skip and duplicate rows as data changes underneath.
6. Performance and reliability
You do not need a load-testing rig to catch the big problems. Note the response time the tester reports for a
typical call and set a rough expectation. Then check the ones that degrade non-linearly: a list endpoint with
a large limit, a search with a broad term, a report over a wide date range. If response time
grows with result count faster than it should, you have found an N+1 query.
Beyond raw latency, verify that pagination is enforced rather than optional, that responses are compressed
(Content-Encoding: gzip or br), that caching headers are present and correct on
cacheable resources, and that rate limiting actually triggers and reports itself with
Retry-After. Measure percentiles rather than averages when you do run load tests — p95 and p99
are what users experience, and a mean can look healthy while one request in twenty times out.
Manual versus automated testing
This is not a competition; the two do different jobs. Manual testing is about discovery — you do not yet know what the endpoint does, or why it is misbehaving, so you need a tight loop of change-send-look. Automated testing is about regression — you already know the correct behaviour and want to be told the moment it changes.
| Manual | Automated | |
|---|---|---|
| Best for | Exploring, debugging, one-off checks, third-party APIs | Regression, CI gates, contract enforcement |
| Feedback speed | Seconds, no setup | Fast once written; slow to write first |
| Repeatability | Poor — depends on memory | Exact, every commit |
| Cost over time | Rises with every repeat | Falls after the first run |
| Catches | Things you didn't think to assert | Only what you asserted |
A workable rule: explore manually, then promote. The moment you check something a second time, write it as a test. The moment a bug is found manually, add a case that would have caught it. Over a few months the automated suite ends up covering exactly the things that actually break, which is a far better selection criterion than trying to design full coverage up front.
Where a browser-based tester fits
The REST API tester is built for the exploratory half. It needs no install, no account and no project file, which matters when you are checking a third-party endpoint once, or handing a reproduction to a colleague. Set the method, URL, headers and auth, send, and read the status, timing and body. When a request is worth keeping, use Copy as cURL and paste it straight into a script, a CI job or a bug report — that button is the bridge from manual to automated, and it is the fastest way to hand someone an exact, runnable reproduction.
For automation, pick whatever suits your stack: a test runner plus an HTTP client in your own language keeps the tests next to the code, schema validation against an OpenAPI document catches drift, and a dedicated load tool covers performance. The important thing is that the suite runs on every commit, not which tool it is written in.
A REST API testing checklist
Work through this per endpoint. Not every line applies to every endpoint, but every line is worth a moment's thought.
Request
- Correct method for the operation, and 405 with an
Allowheader for methods it does not support. - Required parameters enforced; unknown parameters either rejected or documented as ignored.
- Wrong
Content-Typegives 415, not a confusing400. - Query parameters percent-encoded correctly, including spaces,
+and non-ASCII.
Response
- Exact expected status code, not merely a
2xx. Content-Typecorrect and the body parses.- Body validates against the documented schema; field names, types and formats all match.
201responses carry aLocationheader that actually resolves.- No internal or sensitive fields leak into the payload.
- Caching headers appropriate:
ETagwhere useful,no-storeon anything private.
Errors
- Malformed body gives
400, not500. - Missing auth gives
401; insufficient permission gives403. - Unknown identifier gives
404consistently across the API. - Validation failures name the offending field.
- Error body structure identical everywhere; no stack traces or SQL in the message.
Security
- Another user's resource cannot be read or written with your token.
- Tokens expire, and an expired token is rejected.
- HTTPS enforced; plain HTTP redirects or refuses.
- Rate limiting triggers and reports
Retry-After. - CORS grants only the origins you intend — see the headers guide.
Data and lifecycle
- Full create-read-update-delete cycle works end to end and leaves no orphans.
- Pagination returns stable, deterministic ordering with no gaps or repeats.
- Filtering and sorting parameters actually take effect and reject invalid values.
- Repeated
PUTandDELETEare idempotent; repeatedPOSTbehaves as documented. - Concurrent updates are handled, ideally with
If-Matchand 412.
Start testing
Pick one endpoint and walk the checklist. Open the API tester, send the happy path, then deliberately break things one at a time — drop the auth header, corrupt the JSON, ask for a resource that does not exist, request page nine hundred. The interesting results are always in the failures, and an API that answers its failures clearly is one you can build on.
If you are shaky on why a particular method or header behaves the way it does, the HTTP methods reference and the HTTP headers reference cover the underlying semantics, and the status code reference explains any code you meet along the way. Testing an event-driven integration instead of a request-response one? The webhook tester handles the inbound direction.
Frequently asked questions
What is REST API testing?
Verifying that an HTTP API behaves correctly at its interface — right status code, schema-conformant body, correct headers, sensible errors, enforced authorisation. It tests the exposed contract rather than the implementation behind it.
What should I test on every endpoint?
At minimum: the success path, exact status code and Content-Type, body against schema,
missing and invalid authentication, malformed input, a non-existent resource, and the error body shape. Add
pagination and concurrency where relevant.
Manual or automated?
Both. Manual for exploring and debugging; automated for regression. Anything you check twice should become a scripted test.
How do I test endpoints that need authentication?
Use a dedicated test account and a short-lived token. Cover a valid token, a missing or malformed one
(expect 401), and a valid token belonging to a user without permission (expect
403).
What is contract testing?
Validating provider responses and consumer expectations against a shared schema such as an OpenAPI document, so breaking changes like a renamed field are caught before release.
Can I test a localhost API with an online tester?
Not with this one — requests are sent from our server, so loopback and private addresses are blocked to prevent server-side request forgery. Expose the service on a public URL with a tunnelling tool, or use a local client.