What makes an API truly RESTful (Richardson Maturity Model), REST vs "RESTful" services, statelessness trade-offs, partial updates, idempotency, modelling many-to-many relationships, nested resources, pagination (offset vs cursor), HATEOAS, API-first design, and choosing status codes — 201 vs 202, validation failures (400 vs 422), 401 vs 403, 204 for DELETE and whether DELETE returns a body, GET with a body, 409 Conflict, idempotent POST responses, PUT returning 201, and 503 with Retry-After.
Published September 25, 2026
API design questions reward consistency and client empathy. Pick conventions, explain why, and make them uniform across services (a style guide, linting with Spectral). Status codes matter, because clients, gateways and retry logic depend on them.
Short answer: REST (Roy Fielding) is an architectural style, with these constraints:
The Richardson Maturity Model grades HTTP APIs:
/api)./orders/42)."REST service" vs "RESTful web service": they're used interchangeably. Strictly, "RESTful" means adhering to the REST constraints, while many APIs called "REST" are just JSON over HTTP (Level 1–2). Being honest about that nuance is a good answer.
Learn it in depth → API Contract Design
Short answer: Stateless means that each request carries everything the server needs (authentication token, parameters). The server keeps no client session state between requests; the resource state lives in the database.
Short answer: Use PATCH /resources/{id}, with an explicit patch format:
application/merge-patch+json): send only the changed fields, and null removes a field. It's simple, but it can't express "set to null" vs "remove" distinctly, or array element operations.application/json-patch+json): a list of operations (add, remove, replace, move, test). It's precise, with array support, and test gives conditional updates.Design points:
If-Match with an ETag, returning 412 on mismatch, or 428 if it's required and missing);POST /orders/{id}/cancel), not a PATCH of status;Short answer:
Idempotency-Key header (a client-generated UUID):
(key, request fingerprint, response status and body) atomically, with a TTL;Location and body). In-flight duplicates get 409, or wait;PUT /orders/{uuid} (naturally idempotent);Short answer: Either:
PUT /users/{userId}/roles/{roleId} (idempotently add), DELETE /users/{userId}/roles/{roleId} (remove), GET /users/{userId}/roles (list), and the reverse GET /roles/{roleId}/users if it's needed;POST /enrollments {studentId, courseId, …}, GET /enrollments?studentId=…, DELETE /enrollments/{id}.Choose the association resource whenever the link has attributes, a lifecycle or permissions of its own. Support bulk operations for large link sets.
Short answer:
/users/{id}/orders)?Short answer:
/users/{userId}/orders (the user's orders), /orders/{orderId}/lines./users/1/orders/2/lines/3/discounts/4). Once a child has a globally unique ID, give it a top-level URI too (/orders/{orderId}), and use query filters for other views (/orders?userId=1&status=PAID)./users/1/orders/99 must not expose user 2's order 99)./cancel), and a stable ID format.Short answer:
?page=3&size=50, or offset/limit):
?limit=50&cursor=eyJjcmVhdGVkQXQiOi4uLn0):
createdAt plus id), and the next page queries WHERE (created_at, id) < (:ts, :id) ORDER BY created_at DESC, id DESC LIMIT 50;items, nextCursor (opaque, base64 or signed), and optionally prevCursor/hasMore, or Link headers (rel="next"). Cap the page size, and document the sort order.Short answer: Hypermedia As The Engine Of Application State: responses include links (and affordances) describing the available next actions and related resources, so clients discover transitions, instead of hard-coding URLs and business rules:
{ "id": "42", "status": "PAID",
"_links": { "self": {"href": "/orders/42"},
"cancel": {"href": "/orders/42/cancel", "method": "POST"},
"invoice": {"href": "/orders/42/invoice"} } }
Practical uses:
Spring supports it through Spring HATEOAS (HAL, HAL-FORMS). The reality: most internal APIs stay at Level 2, because clients are generated from OpenAPI, and hypermedia adds payload and complexity. It's valuable when workflow logic should be server-driven.
Short answer:
Location header with the new URI (and usually the representation).Location: /jobs/123, or a body with the job ID and a status URL), where the client polls, or gets a webhook.Common trap: returning 201 for work that is only queued. Clients then assume the resource exists.
Short answer:
MethodArgumentNotValidException.Many APIs use 400 for all validation errors, and some use 422 for semantic ones. Consistency matters most. Return Problem Details with field-level errors either way.
Short answer:
WWW-Authenticate header describing how to authenticate. The client should log in or refresh the token.Security nuance: for resources that the caller shouldn't even know exist, return 404 instead of 403, to avoid leaking their existence.
Short answer: HTTP doesn't forbid it, but a body on a GET has no defined semantics (RFC 9110). Proxies, caches, CDNs, load balancers and some clients may drop it, or reject it, and caching keys ignore bodies, so you get unpredictable behaviour. Elasticsearch famously accepts GET bodies, but also accepts POST for the same search. The alternatives for complex queries:
POST /search (a query resource), accepting that it's not cacheable by default;QUERY HTTP method (an IETF draft) for safe, idempotent requests with bodies, once support spreads.Short answer: When the request conflicts with the current state of the resource:
If-Match, prefer 412 Precondition Failed;Include details in Problem Details (the conflicting field, or the current state), so the client can resolve it.
Short answer: Yes. PUT has create-or-replace semantics on a client-specified URI. If the resource didn't exist, and PUT created it, return 201 Created. If it replaced an existing resource, return 200 OK (with the body) or 204 No Content. For example, PUT /configurations/{tenantId}/theme, or PUT /orders/{clientGeneratedUuid}. This also gives natural idempotent creation.
Retry-After header?Short answer: 503 Service Unavailable means the server is temporarily unable to handle the request: overload, maintenance, a dependency outage or circuit breaker open, or a readiness failure. It signals that the problem is transient, so clients may retry. Retry-After (seconds, or an HTTP date) tells clients when to retry. It's used with 503 (maintenance windows, load shedding) and 429 Too Many Requests (rate limits). Well-behaved clients and SDKs honour it, instead of hammering the server. Distinguish it from 502/504 (upstream gateway errors) and 500 (unexpected server bugs, which usually aren't worth blind retries).
Short answer: Designing the API contract before implementing it:
The benefits:
Q: What's the difference between 200 and 204 for updates? A: 200 returns the updated representation in the body (convenient for clients). 204 returns nothing (less payload). Choose per API style, and be consistent.
Q: How should errors be formatted?
A: RFC 9457 Problem Details (application/problem+json): type, title, status, detail, instance, plus extensions (errors, traceId, code). Use a consistent format across all services.
Q: What are ETags for?
A: Caching (If-None-Match gives 304 Not Modified) and optimistic concurrency (If-Match gives 412 on mismatch). Spring supports shallow ETags with ShallowEtagHeaderFilter, or explicit ETags through ResponseEntity.eTag(...).
Q: What status code do you use when a rate limit is hit?
A: 429 Too Many Requests, with Retry-After, and optionally RateLimit-* headers (an IETF draft) describing the quota and reset time.