System Design · Module 2 · Lesson 2 of 4 · 40 min
HTTP Methods Status Codes and Resource Contracts
Design a small resource API with clear ownership, validation, success responses and failures a caller can handle.
01 The idea
The problem this lesson solves
Two developers can agree to “build a notes API” and still implement incompatible behavior. One returns a new note id after saving; the other returns success before persistence. One trusts owner_id in the body; the other derives it from authentication. An API contract resolves these disagreements by defining the operation, its authority, the representation and every meaningful outcome.
Use the requirements and ownership vocabulary from module 1. This module follows one request across the API boundary.
02 Worked example
Create a note and protect its ownership
03 The method
Understand the mechanism
Design resources around identities and access patterns
A resource is a named thing or conceptual entity exposed through the interface. /v1/notes can represent a collection, while /v1/notes/n7 identifies a particular note. The identifier should remain stable when mutable fields such as title change. A title is a poor durable identifier if users can rename it.
Start with operations the client needs: create a note, retrieve it, change its body, delete it and list accessible notes. For each, identify the caller, target and ownership check. The database access should follow the same identity. A note lookup by id followed by an authorization decision is different from a query already restricted to the authenticated owner.
Resources do not require exposing table names or internal service topology. A client-facing representation may combine several stored fields or omit internal metadata. This separation lets storage evolve while preserving the public contract.
Use versioning and compatibility deliberately. Adding an optional response field is often easier for tolerant clients than renaming an existing field or changing its meaning. New required request fields can break older callers. Define how unknown fields, omitted fields and null values are treated rather than allowing accidental behavior to become the contract.
Match method semantics to intended effects
GET retrieves a representation and should not be designed to perform a user-requested state-changing action. A server may still log access or update internal accounting. The important distinction is the operation the client requested, not whether the server executes literally zero writes.
POST commonly submits to a collection or invokes an operation whose result is determined by the server. PUT supplies a replacement representation at a known target under its contract. PATCH describes a partial change, whose idempotency depends on the patch semantics. DELETE requests removal of the target’s current association or representation according to the service’s rules.
Idempotent means repeating the same request has the same intended effect as making it once. It does not mean every attempt returns an identical status or body. A first DELETE might return 204 and a later attempt 404 after the resource is gone. A “set title to X” patch differs from an “append X to title” patch under repetition.
Method choice communicates useful expectations to clients and intermediaries, but it does not repair incorrect business logic. A handler named PUT that increments a balance on every retry violates its intended semantics. Review what the stored state actually does.
Make success visible and precise
For POST /v1/notes, the request body might contain title and body. The server derives owner_id from the authenticated context. After durable creation under the chosen contract, it returns 201 Created, a representation containing the new id, and Location identifying the resource.
Specify whether returning success means the work is complete or only accepted. A long export request might return 202 with an operation resource the caller can poll. That response must not claim the file already exists. A queue-backed operation needs explicit terminal states, failure reporting and retention of its outcome.
For a note update, include a version or ETag when concurrent editing matters. A client can submit an If-Match condition so the server refuses an update based on an old representation rather than silently overwriting a newer edit. The database transition must enforce the condition; checking a version and then writing unconditionally reintroduces a race.
A response should contain the information needed for the next step. Returning only “success” forces the client to guess the new identity or perform an extra ambiguous search. Returning internal stack traces provides the wrong kind of detail and can expose implementation information without helping the caller recover.
Separate authentication, authorization and validation
Authentication establishes the caller’s identity. Authorization decides whether that caller may perform this operation on this resource. Validation checks whether the submitted data satisfies the operation’s shape and business constraints. Passing one does not imply passing the others.
An authenticated user may still lack permission to edit n7. A well-formed note body may still exceed the account’s policy limit. A request that supplies owner_id=another_user must not gain authority merely because the value is syntactically valid.
Authorization applies to every object access, including list filters, nested fields and bulk operations. Knowing or guessing an id is not permission. Decide whether unauthorized access reveals existence; some APIs use a missing response to avoid disclosing a protected resource. Keep that policy consistent across endpoints.
HTTPS protects the transport under its trust model. It does not ensure the logged-in account may access the requested note. Likewise, CORS is a browser access mechanism rather than a substitute for server-side authentication. An API must enforce authority for callers that do not use a browser too.
Design errors as part of the interface
A useful error response combines an appropriate HTTP status with a stable machine-readable code and a safe human-readable explanation. For example, a conflicting edit can identify version_conflict and explain that the caller should fetch the current representation. The client should not need to parse prose to determine its recovery action.
Distinguish malformed input, missing or invalid authentication, insufficient permission, missing resources, state conflicts, quota exhaustion and service failure. These conditions have different remedies. Retrying an unchanged malformed body will not repair it; retrying an unavailable dependency may help if the operation is retry-safe and still within its deadline.
Do not return 200 for every outcome and bury the real result in a string field. Infrastructure, clients and monitoring often rely on status semantics. Conversely, do not assume a success status proves business correctness: a confirmation for the wrong note is still an application defect.
Set limits explicitly. Maximum body size, page size and field lengths protect both reliability and predictability. Return a documented error when the request exceeds them. Silent truncation can corrupt the caller’s understanding of what was stored.
Evolve and test the contract through examples
Write a complete exchange for the happy path and one for each important failure. This exposes omissions that a list of endpoint names misses. A create exchange needs request content type, identity context, body, status, Location and response fields. An update conflict needs a recovery path.
Then test changes against existing clients. If a field becomes optional, an old client may still assume it always exists. If a list’s ordering changes, cursor behavior and UI pagination may break even though the JSON shape is unchanged. Compatibility includes semantics, not only syntax.
Contract tests should exercise permissions and repeated requests as well as serialization. Create as A, attempt to read or edit as B, update with a stale version, submit an oversized body, and repeat a request after a response loss. These cases connect the interface to the invariants introduced earlier.
You have a usable API design when a caller can determine what to send, what success means, what each failure means, and whether and how to retry. The implementation can change internally while those promises remain stable.
Guided tutorial
Write the creation exchange
Draft POST /v1/notes with title and body, an authenticated caller A, and Content-Type: application/json. Return 201 with id n7 and Location: /v1/notes/n7 after the save commits. Leave owner_id out of the caller-controlled authority decision. Explain which component supplies it.
Try a different caller
Repeat GET and PATCH for n7 as caller B. Decide whether the contract returns forbidden or deliberately hides existence. Apply the same policy to the list endpoint so a bulk listing does not reveal objects the direct endpoint protects.
Prevent an overwritten edit
Assume n7 is version 4. Two clients read it. Client one writes version 5. Client two submits an update conditional on version 4 and must receive a conflict/precondition outcome instead of overwriting version 5. Describe the conditional database update that makes the check atomic.
Build an outcome table
Include valid creation, malformed body, expired authentication, unauthorized access, stale version and unavailable storage. For every row, write a status, stable error code if needed and caller action. Mark which requests can be retried unchanged and which require new input or authentication.
HTTP contract sketch
POST /v1/notes → 201 Created + Location
GET /v1/notes/n7 → 200 or documented failure
DELETE /v1/notes/n7 → deletion outcome
error body: {"code":"permission_denied","message":"..."}
owner_id comes from authentication, not the request bodyCheck your result
Creation returns a resource identity and Location. Reading another user’s note requires an explicit permission decision. Changing owner_id in the request body must not transfer authority.
04 Cheat sheet
Review the decisions
Use this table to explain the mechanism and its limit without memorizing a product name.
| Choice or concept | Remember | Check |
|---|---|---|
| GET | Read a resource | Safe and idempotent semantics |
| POST | Create or invoke | Needs an explicit retry contract |
| PUT | Replace at a known resource | Idempotent intended effect |
| DELETE | Remove a resource | Repeated status codes may differ |
05 Where & why
Check the primary references
Use official references for protocol and implementation details. The questions here are teaching prompts.
06 Interview questions
Explain the reasoning out loud
Answer before opening the explanation. The questions progress from vocabulary to decisions and failure behavior.
What does Resource mean?
What does Safe method mean?
What does Authorization mean?
Is DELETE idempotent if the second response differs?
Does an unpredictable id authorize access?
What does 202 establish?
Why use structured error codes?
Can PATCH always be retried?
07 Practice problems
Practice a new case
Solve the new cases before revealing hints. Complete worked solutions follow the set.
Create outcome
EasyShow the hint
Permission failure
EasyShow the hint
Retry semantics
MediumShow the hint
Async work
MediumShow the hint
Bound a request
MediumShow the hint
Review a mutable redirect
HardShow the hint
Worked solutions
1. Create outcome
201 Created with a body containing the task id and a Location header is a clear contract.
2. Permission failure
Return the documented permission failure, commonly 403, or a deliberate privacy-preserving 404 policy. Enforce it on the server.
3. Retry semantics
Setting the same value is idempotent under that contract; an increment repeats its effect unless deduplicated.
4. Async work
Return 202 with an operation id and status URL. Define pending, succeeded, failed and expiry behavior for the status resource.
5. Bound a request
Limit page size, query length/complexity and execution time. Also cap body size and enforce authorization filters on all pages.
6. Review a mutable redirect
Use a documented temporary redirect and explicit cache policy consistent with the update visibility requirement. Define the update response, authorization, invalidation path and behavior of already-in-flight reads.