HTTP methods and status codes for SA interviews

Train for your next tech interview
1,500+ real interview questions across engineering, product, design, and data — with worked solutions.
Join the waitlist

Why interviewers ask

A systems analyst writes the API contract that ten other people implement against. If your spec says "POST /users/123/delete returns 200 with body {success: true}", that's four mistakes in one line: wrong method, wrong code, wrong semantics, wrong contract shape. Interviewers at Stripe, Airbnb, and Uber will stop the conversation right there — they are testing whether you can be trusted to design something a mobile client and a payments service both have to obey.

The load-bearing reason this matters: the mobile team retries on 5xx, the backend returns 5xx for business validation like "insufficient funds", and the retry duplicates the charge attempt. The postmortem is brutal, and the trail leads back to a spec written by an analyst who picked the wrong code. The same logic explains why 401 versus 403 matters more than it looks — a misclassified auth error puts the client into an infinite refresh-token loop.

Methods and their properties

Method Purpose Safe Idempotent Body
GET Read yes yes no
HEAD Read metadata (no body) yes yes no
OPTIONS Supported methods on a resource yes yes no
POST Create / non-standard action no no yes
PUT Full resource replacement no yes yes
PATCH Partial update no no* yes
DELETE Remove resource no yes rare

*PATCH is not formally idempotent — it depends on the body. A body of {"status": "active"} is effectively idempotent; a body of {"counter": "+1"} is not. This nuance comes up in senior SA loops at Stripe and Snowflake.

Safe means the call does not change server state. Safe methods can be cached, prefetched, and replayed by a proxy without harm. Idempotent means calling the endpoint N times with the same input has the same effect as calling it once — so the client can safely retry on a network blip.

A classic trap: "Can I use POST for reads?" Technically yes, if the query has too many parameters for a URL. Semantically no — it kills caching and makes retries dangerous. The right answer: "you can, but the cost is paid by every consumer of the API forever, so don't."

Idempotency and safe methods

GET is idempotent by nature — it only reads. Retrying on a 503 is safe by default.

PUT is idempotent because it replaces the resource wholesale. PUT /users/123 with the same body, called once or five times, leaves the row in the same final state. This is why PUT is the right verb for "set this configuration to exactly X" and the wrong verb for "increment this counter".

DELETE is idempotent in a slightly different sense: the first call removes the resource, and subsequent calls hit a 404, but the system state is the same after each call. Returning 200 or 204 the first time and 404 afterward is conventional and acceptable.

POST is the odd one out — every POST /orders creates a new order. Retrying without protection creates duplicates, which is how a billing service ends up charging a card twice for the same checkout.

Load-bearing trick: in payments, Idempotency-Key is the difference between "we lost one transaction" and "we charged every customer twice during the outage." Make it required, not optional.

Idempotency-Key is a request header that makes POST behave like an idempotent call:

POST /payments
Idempotency-Key: 8a1b2c3d-...
Content-Type: application/json

{
  "amount": 1000,
  "currency": "USD",
  "to": "user-123"
}

The server stores the response of the first POST with that key and returns the same response for any later request carrying it — without re-running the action. Stripe popularized this pattern, and it is now standard at every fintech. The spec must describe how long results are cached and how to handle conflicts (same key, different body — return 422).

2xx and 3xx codes

200 OK — success with a body. The universal default.

201 Created — a new resource was created. Return with a Location: /users/123 header pointing at the new URI. Used for POST and for PUT-that-creates.

202 Accepted — the request was accepted and is being processed asynchronously. The body usually contains a link to a status endpoint like /jobs/abc/status. This is the right code for "we will run a long migration and email you when it's done."

204 No Content — success with no body. Convenient for DELETE and for PUT where the client does not need the updated representation back.

The 3xx family shows up less often in JSON APIs, but you still need to know:

  • 301 Moved Permanently — old URL is dead forever, cache aggressively. Used for SEO and domain moves.
  • 302 Found / 307 Temporary Redirect — temporary redirect; 307 preserves the method, 302 historically did not.
  • 304 Not Modified — paired with If-None-Match/ETag, lets the server skip sending the body when nothing changed.

In a typical CRUD JSON API you will mostly use 200, 201, and 204. Everything else has a specific job.

Train for your next tech interview
1,500+ real interview questions across engineering, product, design, and data — with worked solutions.
Join the waitlist

4xx codes that get misused

This is where interviewers find out whether you actually wrote a spec or just translated a Jira ticket. The most common confusions:

400 Bad Request versus 422 Unprocessable Entity:

  • 400syntactic failure: malformed JSON, missing required field, wrong type. The server cannot parse the request.
  • 422 — syntax is fine, but the semantics are invalid: "end date is before start date", "email already taken", "insufficient funds".

Not every API uses 422. Older designs return 400 for everything. In an interview you can say: "Modern practice is 422 for business validation and 400 for parsing — but I check the existing conventions of the API I'm extending before introducing a new code."

401 Unauthorized versus 403 Forbidden:

  • 401 — no valid credentials: missing token, invalid token, expired token. The client should refresh and retry. The server must include a WWW-Authenticate header per RFC 9110.
  • 403 — credentials are valid, but the user is not allowed to access this resource. Retrying with the same token will not help; the user needs different permissions.

Mnemonic: 401 is "I don't know who you are", 403 is "I know who you are, but you can't have it."

404 Not Found versus 403: in sensitive APIs (banking, healthcare), return 404 instead of 403 to hide existence. If an attacker sees 403 on /users/12345, they know that user exists. With 404, they do not. State this explicitly in the spec.

409 Conflict — conflict with the current state of the resource. Classic uses: optimistic lock failure (If-Match did not match the current ETag), duplicate Idempotency-Key with a different body, registration with an email already in use (if you do not prefer 422 for that case).

429 Too Many Requests — rate limit exceeded. Always return with a Retry-After header so the client knows how long to back off.

5xx codes and retry rules

500 Internal Server Error — an unhandled exception on the server. The body must not leak stack traces or internal field names. A 500 body that contains "NullPointerException at OrderService.java:142" is a security finding in any production review.

502 Bad Gateway — a proxy or load balancer got an invalid response from an upstream service.

503 Service Unavailable — the service is temporarily down (maintenance, overload). Return Retry-After so clients back off.

504 Gateway Timeout — the upstream did not respond before the proxy gave up.

The retry rules that the contract has to make explicit:

  • 5xx — retry with exponential backoff (1s, 2s, 4s, 8s, with jitter).
  • 408, 429, 503, 504 — retry, honoring Retry-After when present.
  • 4xx other than 408 and 429 — do not retry; the client is wrong, retrying will not fix it.

Sanity check: only retry idempotent requests (GET, PUT, DELETE) or POST requests carrying an Idempotency-Key. Retrying a bare POST is how you ship a duplicate-charge incident.

The anti-pattern that has burned every fintech at least once: returning 500 on a business error. "Insufficient funds" is not a server failure — it is a 422 or 409 depending on flavor. Return 500 and the client's retry layer will dutifully try to debit the card eight times before giving up.

Common pitfalls

The most common pitfall is using GET with a request body. RFC 9110 permits it, but proxies, CDNs, and HTTP libraries are allowed to strip the body, and many do. If your filter parameters do not fit in a query string, the right answer is a POST /search endpoint with documented non-idempotent semantics. Specs that mandate GET-with-body are a tell that the analyst has never debugged a Cloudflare 502.

Another trap is using PUT for partial updates. PUT is a full replacement — fields omitted from the body are erased. To change one field, use PATCH with either JSON Merge Patch (RFC 7396) or JSON Patch (RFC 6902), and state in the contract which one. The wrong dialect creates silent data loss when clients send {"phone": null} meaning "leave it alone" and the server reads it as "set to null".

A perennial nightmare is returning 200 with {"success": false} for errors. Naive clients parse status == 200 as success and never look at the payload. Errors slip past monitoring because nothing logs a non-2xx, and on-call gets paged days later by a customer complaint. The status code is part of the contract — it must reflect the outcome.

Confusing 401 and 403 in the spec sends mobile clients into an infinite refresh loop. On 401 the client refreshes the token and retries; on 403 the client shows "no access." Swap them and a forbidden user keeps minting new tokens forever, burning auth quotas.

Returning 500 for business validation is the bug that ships duplicate charges. Every layer of client retry logic — mobile SDK, gateway, even Cloudflare — will retry, and each retry is another attempt to debit the card. The fix is mechanical: anything the user could fix is a 4xx; anything we broke is a 5xx.

Skipping Idempotency-Key in payment specs is the SA-level mistake that causes the biggest incident bills. Without it, any network timeout on a POST turns into a duplicate transaction. The contract should require the header on every non-idempotent write that touches money or external systems.

If you want to drill SA contract questions like this every day, NAILDD is launching with 500+ interview problems built around exactly this pattern.

FAQ

Should I use POST /users/{id}/delete or DELETE /users/{id}?

DELETE is the REST-standard answer. POST against a verb endpoint is the old RPC style; it breaks HTTP semantics, defeats caching middleware, and signals to interviewers that you have not internalized the method table. The only legitimate exception is when an action cannot be modeled as CRUD — POST /payments/{id}/refund is acceptable because "refund" is not "update the payment", it is a separate side-effecting operation. Even then, document it clearly.

Can I return 201 on a PUT?

Yes, when the PUT creates a resource. Classic PUT semantics are "create or replace at this exact URI" — if no resource existed, the server creates one and returns 201 Created with a Location header. If the resource already existed and was replaced, return 200 with the updated body or 204 with no body. In an interview, explicitly call out the branch: "PUT can both create and replace; the status code depends on which path executed."

What should POST /orders return on creation?

201 Created, with the new order in the body and a Location: /orders/{id} header pointing at the canonical URI of the new resource. If creation is asynchronous and you do not know the URL yet, return 202 Accepted with a status URL in the body. Returning a plain 200 with the new order is not wrong, but it is sloppy — you are throwing away contract information that clients can use to deduplicate and link.

How is PATCH different from PUT?

PUT replaces the resource as a whole — every field in the body is the new state, and any field you omit is wiped. PATCH only touches the fields you send. The catch is that PATCH requires a format — usually JSON Merge Patch (RFC 7396), or JSON Patch (RFC 6902) when you need operations like array element removal. The safe interview answer: "Merge Patch unless we need array surgery, then JSON Patch."

How do I describe a race condition in a spec?

Through optimistic locking. The client reads the resource and gets an ETag. On PUT or PATCH, it sends If-Match: <ETag>. If the version matches, the write goes through. If not, the server returns 412 Precondition Failed (or 409 Conflict). The spec must describe client behavior on conflict: re-read, re-apply, retry. Without it documented, every team reinvents it badly.

Is this the official source of truth?

No. This article reflects RFC 9110 (HTTP Semantics), RFC 5789 (PATCH), the IETF draft on Idempotency-Key headers, and conventions used by Stripe and GitHub. Specific company APIs may diverge — always read existing conventions before introducing a new code.