Skip to content
koducathome

How to Test a REST API: Methods, Headers, Status Codes and CORS

A practical walkthrough of REST API testing: HTTP methods and idempotency, status code classes, request headers, CORS preflight and repeatable checks in code.

By the koducat teamUpdated 6 min read

What testing a REST API means

Testing a REST API means sending real HTTP requests and checking that the responses are what clients expect: the right status code, the right headers and a body with the right shape. You do this while building an endpoint, when debugging a failing integration, and before relying on a third-party API in production.

A useful mental model is that every request has four parts you control — the method, the URL (including the query string), the headers and the body — and every response has three parts you check: the status code, the headers and the body. Most API bugs come down to one of those seven things being different from what someone assumed.

HTTP methods, safety and idempotency

HTTP semantics (RFC 9110) define two properties that matter for testing. A safe method should not change server state. An idempotent method can be repeated with the same effect as sending it once — which is what makes it safe for clients and proxies to retry after a timeout.

MethodTypical useSafeIdempotent
GETRead a resource or collectionYesYes
HEADSame as GET but headers onlyYesYes
OPTIONSAsk which methods/headers are allowed (used by CORS preflight)YesYes
POSTCreate a resource or trigger an actionNoNo
PUTReplace a resource at a known URLNoYes
PATCHPartially update a resourceNoNot guaranteed
DELETERemove a resourceNoYes

Idempotent describes the effect on the server, not the response: deleting the same resource twice may return 204 and then 404, and that is still idempotent. When testing, send PUT and DELETE requests twice and confirm the second call does not create duplicates or fail in an unexpected way. For POST endpoints that must be retry-safe — payments are the classic example — many APIs accept an idempotency key header so the server can recognise a repeated request.

Reading status codes

The first digit of a status code tells you its class. Check the class first, then the exact code.

ClassMeaningCodes you will meet often
1xxInformational100 Continue, 101 Switching Protocols
2xxSuccess200 OK, 201 Created (often with a Location header), 204 No Content
3xxRedirection301/308 permanent, 302/307 temporary, 304 Not Modified
4xxClient error — fix the request400, 401, 403, 404, 409 Conflict, 422, 429 Too Many Requests
5xxServer error — the request may be fine500, 502 Bad Gateway, 503 Service Unavailable, 504 Gateway Timeout
  • 401 vs 403: 401 Unauthorized means the request has no valid credentials — log in or send a token. 403 Forbidden means the server knows who you are and still refuses.
  • 400 vs 422: many APIs use 400 for malformed syntax and 422 for well-formed data that fails validation. Read the body; good APIs explain which field failed.
  • 429 and 503: check for a Retry-After header and wait at least that long before retrying.

Testing an endpoint step by step

  1. Open the API Tester, choose the method and paste the full URL. If the query string is long, check it in the URL Parser first.
  2. Add the headers the API needs. Most JSON APIs need Content-Type: application/json on requests with a body, Accept: application/json, and usually Authorization: Bearer <token>.
  3. For POST, PUT and PATCH, add a JSON body. Validate it before sending — a stray trailing comma produces a confusing 400 from the server.
  4. Send the request and check the status code first, then the response headers (Content-Type, caching headers, rate-limit headers), then the body.
  5. Repeat with bad input on purpose: a missing field, a wrong type, an expired token. Error responses are part of the API contract too.
  6. Once it works, export the request as code with cURL to Code so you can repeat the test from a script or CI job.
A browser-based tester is subject to the browser's CORS rules, while command-line tools like curl are not. If a request works in curl but fails in the browser with a network error, the API is probably missing CORS headers — see the next section.

Understanding CORS and preflight requests

Cross-Origin Resource Sharing (CORS) is a browser security mechanism. When JavaScript on https://app.example.com calls https://api.example.com, the browser only lets the page read the response if the API answers with an Access-Control-Allow-Origin header that permits that origin. The server still receives and processes the request — CORS controls what the calling page may read, not what the server accepts.

For requests that are not "simple", the browser first sends a preflight: an OPTIONS request asking for permission. A preflight is triggered when the method is anything other than GET, HEAD or POST; when the request sets headers outside the CORS-safelisted set (for example Authorization or a custom X- header); or when Content-Type is anything other than application/x-www-form-urlencoded, multipart/form-data or text/plain — which includes application/json.

Simulate a preflight with curl
curl -i -X OPTIONS https://api.example.com/orders \
  -H "Origin: https://app.example.com" \
  -H "Access-Control-Request-Method: POST" \
  -H "Access-Control-Request-Headers: content-type, authorization"

A correct response includes headers like these. If any are missing or do not match, the browser blocks the real request:

HTTP/1.1 204 No Content
Access-Control-Allow-Origin: https://app.example.com
Access-Control-Allow-Methods: GET, POST, PUT, DELETE
Access-Control-Allow-Headers: content-type, authorization
Access-Control-Max-Age: 600
Vary: Origin

When the request includes credentials (cookies, or fetch with credentials: "include"), Access-Control-Allow-Origin cannot be *; it must echo the specific origin and the response must also send Access-Control-Allow-Credentials: true. Use the HTTP Header Inspector to review response headers side by side.

Making tests repeatable in code

Manual testing finds problems; scripted tests stop them coming back. Keep the checks focused on the contract — status, key headers and the fields clients depend on.

curl: show status and headers, fail on HTTP errors
curl -sS -i https://api.example.com/users/42 -H "Accept: application/json"

# Exit with a non-zero code on 4xx/5xx — handy in CI scripts
curl -sS --fail-with-body https://api.example.com/health
TypeScript with fetch (Node 18+ or browser)
const res = await fetch("https://api.example.com/users", {
  method: "POST",
  headers: { "Content-Type": "application/json", Accept: "application/json" },
  body: JSON.stringify({ name: "Ada", email: "ada@example.com" }),
});

// fetch only rejects on network errors — check the status yourself
if (res.status !== 201) {
  throw new Error(`Expected 201, got ${res.status}: ${await res.text()}`);
}
const user = await res.json();
console.assert(typeof user.id === "string", "id should be a string");
Python with requests
import requests

res = requests.get("https://api.example.com/users/42", timeout=10)
assert res.status_code == 200, res.text
assert res.headers["Content-Type"].startswith("application/json")
assert "email" in res.json()

For more request-building and debugging utilities, browse the API tools category.

Try it now with the free API Tester — it runs in your browser.

Open API Tester

Frequently asked questions

Why does my request work in curl but fail in the browser?

Browsers enforce CORS and curl does not. If the API does not return the right Access-Control-Allow-* headers for your page's origin — including on the OPTIONS preflight — the browser blocks the response. Fix it on the server, or call the API from your own backend.

Is PATCH idempotent?

Not necessarily. A PATCH that sets a field to a value is idempotent in practice, but one that appends to a list or increments a counter is not. HTTP semantics do not guarantee it, so clients should not blindly retry PATCH requests.

Should an API return 200 with an error message in the body?

It is better not to. Clients, proxies and monitoring tools rely on the status code to detect failures. Return a 4xx or 5xx code and describe the problem in the body, ideally in a consistent format such as RFC 9457 problem details.

What is the difference between 404 and 410?

404 Not Found says nothing about whether the resource ever existed or may come back. 410 Gone says it existed and has been permanently removed.