When should I use POST versus PUT to create a resource?
Use POST when the server assigns the resource identifier and PUT when the client controls it. POST /orders lets the server generate the new order's ID and return it in a 201 Created response with a Location header; this is the common case for collections. PUT /orders/{id} creates or fully replaces the resource at a client-chosen URI and is idempotent, which makes it safe to retry - useful when the client can deterministically derive the ID. If you POST to create, guard against duplicate submissions with an idempotency key, since POST is not idempotent.
How do I version an API without breaking existing clients?
Pick one versioning scheme and apply it consistently: URI versioning (/v1/orders) is the most visible and cache-friendly, while header or media-type versioning (Accept: application/vnd.example.v2+json) keeps URLs stable. Within a major version, only make backward-compatible changes - add optional fields and new endpoints, never remove or rename fields or tighten validation. Reserve breaking changes for a new major version, announce deprecations with the Deprecation and Sunset headers and a migration guide, and keep the old version running through a stated window. The goal is that a client written against v1 keeps working until you deliberately retire v1.
Which HTTP status code should I return for a validation error?
Return 400 Bad Request for a malformed or semantically invalid request the client must fix, and 422 Unprocessable Content if you want to distinguish well-formed syntax that fails business validation. Use 401 Unauthorized when authentication is missing or invalid, 403 Forbidden when the authenticated caller lacks permission, 404 Not Found for a missing resource, and 409 Conflict for a state conflict such as a duplicate or version mismatch. Pair the status with a machine-readable error body - ideally RFC 9457 Problem Details - so clients can react programmatically. Never return 200 OK with an error embedded in the body, because that defeats every generic client and proxy.
What is idempotency and why does it matter for retries?
An operation is idempotent if performing it many times has the same effect as performing it once, so a client can safely retry after a network timeout without fear of duplicate side effects. GET, PUT, and DELETE are idempotent by definition; POST is not. Because networks drop responses, any unsafe operation a client might retry - creating a payment, sending a message - should accept an idempotency key (e.g. a UUID in an Idempotency-Key header) that the server stores so a repeated request returns the original result instead of acting twice. Designing for retries is the difference between an API that survives real-world flakiness and one that double-charges customers.
Should I use offset or cursor pagination?
Use cursor (keyset) pagination for large or frequently changing datasets and offset pagination only for small, stable lists where jumping to an arbitrary page matters. Offset pagination (?limit=20&offset=100) is simple but degrades on large offsets and can skip or duplicate rows when items are inserted or deleted between page requests. Cursor pagination returns an opaque cursor pointing just past the last item, so the next page seeks by a stable sort key and stays both correct and fast regardless of inserts. Whichever you pick, document it, cap the maximum page size, and return the cursor or links in a consistent place.
How should I design error responses?
Standardise on RFC 9457 Problem Details (application/problem+json) so every error has the same shape: type (a URI identifying the error class), title, status, detail, and instance, plus any domain-specific extension fields. Use the correct HTTP status code as the primary signal and the body for the human- and machine-readable specifics, such as which fields failed validation. Keep messages safe - never leak stack traces, SQL, or internal identifiers - and keep the type URIs stable so clients can branch on them. A consistent error contract lets a client handle failures from many endpoints with one code path.
How do I secure an API endpoint?
Serve everything over TLS, authenticate every request, and authorize every action separately. Authentication establishes who the caller is - commonly an OAuth 2.0 access token or signed JWT passed as a bearer token - and you must validate the token's signature, expiry (exp), issuer (iss), and audience (aud) on every request. Authorization then checks whether that caller may perform the specific operation on the specific resource, enforced server-side and never trusted from the client. Add rate limiting, validate and bound all input at the boundary, and avoid leaking sensitive data in error messages.
What is HATEOAS and do I actually need it?
HATEOAS is the REST constraint where responses include links to the valid next actions, so clients navigate the API by following links instead of constructing URLs from out-of-band knowledge. In theory it lets servers change URL structures without breaking clients and makes APIs self-describing. In practice it is the least adopted REST constraint because most clients are written against fixed documentation and the extra link machinery adds cost. Adopt it when you have many long-lived clients you cannot coordinate with, or a workflow with state-dependent transitions; skip it for an internal API consumed by a generated SDK.
How should I implement rate limiting and communicate it to clients?
Choose an algorithm (token bucket and sliding window are common), set limits per client or API key, and reject excess traffic with 429 Too Many Requests. Tell clients where they stand by returning RateLimit-Limit, RateLimit-Remaining, and RateLimit-Reset headers on normal responses, and include a Retry-After header on the 429 so a well-behaved client knows exactly when to retry. Document the limits and provide a path to request higher quotas. The aim is fair access and overload protection while making the limits predictable enough that clients can self-throttle rather than be surprised.
REST, GraphQL, or gRPC - how do I choose?
Default to REST over JSON for public, browser-facing, and broadly consumed APIs because it is cacheable, debuggable with standard tooling, and universally understood. Choose GraphQL when clients need to fetch deeply nested, client-specified field selections in one round trip and you can afford the server-side cost of query planning, depth limiting, and breaking HTTP caching. Choose gRPC for low-latency, high-throughput internal service-to-service calls where a typed .proto contract and HTTP/2 streaming pay off and browser reach is not a concern. The styles are not mutually exclusive: many systems expose REST externally and gRPC internally.
How do I design a good API URL and resource hierarchy?
Model resources as plural nouns and use HTTP methods as the verbs: GET /orders, POST /orders, GET /orders/{id}, DELETE /orders/{id}. Express containment with nested paths (/orders/{id}/items) but avoid nesting more than one or two levels deep, since deep hierarchies become brittle; reference other resources by ID instead. Keep names consistent (one casing convention, plural collections), avoid verbs in paths, and put filtering, sorting, and pagination in the query string rather than the path. Predictable, noun-based URLs make an API discoverable and let clients guess correctly.
What belongs in the query string versus the path?
The path identifies a specific resource or collection; the query string refines a collection request. Put identity in the path (/orders/{id}) and put filtering, sorting, field selection, and pagination in the query string (/orders?status=open&sort=-createdAt&limit=20). Query parameters should be optional and order-independent, and a request with no query parameters should return a sensible default page. Keeping this separation clean means caching, routing, and documentation all behave predictably.
How do I handle partial updates - PUT or PATCH?
Use PUT when the client sends a complete representation that fully replaces the resource, and PATCH when it sends only the fields to change. PUT is idempotent and conceptually simple but forces clients to send (and risk overwriting) every field. PATCH is more bandwidth-efficient for large resources; choose a defined patch format such as JSON Merge Patch (RFC 7386) for simple field updates or JSON Patch (RFC 6902) for precise array and nested operations. Combine PATCH with optimistic concurrency (If-Match on an ETag) to avoid lost updates when two clients edit concurrently.
How do I prevent lost updates when two clients edit the same resource?
Use optimistic concurrency control with conditional requests. The server returns an ETag for the current version of a resource; when a client updates, it sends that value in an If-Match header. If the resource has changed in the meantime its ETag will differ, and the server rejects the write with 412 Precondition Failed, prompting the client to refetch, reconcile, and retry. This avoids the lost-update problem without holding long-lived locks, and it scales because the check is a cheap version comparison rather than a pessimistic lock.
Should an API be stateless, and what does that mean in practice?
Yes - statelessness is a core REST constraint and means each request must carry everything the server needs to process it, with no per-client session stored in server memory between calls. In practice the client sends credentials (a token) on every request, and any application state lives on the client or in a shared store like a database or cache, not pinned to one server instance. Statelessness lets any instance handle any request, which is what makes horizontal scaling, load balancing, and failover straightforward. It is also why bearer tokens, rather than server-side sessions, dominate API authentication.
How do I make webhooks reliable and secure?
Sign every webhook payload (commonly an HMAC over the body using a shared secret in a signature header) so the receiver can verify authenticity, and reject unsigned or mismatched calls. Include a unique event ID and timestamp so the consumer can dedupe and ignore replays, since you should assume at-least-once delivery and retry failed deliveries with exponential backoff. Expect the receiver to respond quickly with 2xx and do heavy work asynchronously, and provide a way to replay missed events. Treat the receiver's endpoint as untrusted infrastructure that may be slow or down, and design retries and dead-lettering accordingly.