HTTP Methods and Status Codes: The Full Picture
Most developers know GET and POST. Some know DELETE. A smaller group has a handle on when to use PUT versus PATCH. Almost nobody remembers what OPTIONS does or why HEAD exists.
That's fine for consuming APIs. But when you're designing one, partial knowledge is dangerous. A wrong method choice breaks client caching. A wrong status code misleads callers into retrying something they shouldn't — or not retrying something they should. These aren't cosmetic issues. They're the kind of bugs that take hours to diagnose in production because everything looks fine until the edge case hits.
In the previous article, you designed TaskFlow's resource tree — the nouns. Now we're completing the picture with the verbs. By the end of this article, you'll know exactly which HTTP method to reach for in every situation, what every status code family means, and how to pick the right code for any response your API might return.
Let's go through the full vocabulary.
Quick Reference
The nine HTTP methods:
| Method | Safe? | Idempotent? | Use For |
|---|---|---|---|
GET | ✅ | ✅ | Retrieve a resource |
HEAD | ✅ | ✅ | Retrieve headers only (no body) |
OPTIONS | ✅ | ✅ | Discover what methods a resource supports |
POST | ❌ | ❌ | Create a new resource or trigger a process |
PUT | ❌ | ✅ | Replace a resource entirely |
PATCH | ❌ | ❌ | Partially update a resource |
DELETE | ❌ | ✅ | Remove a resource |
TRACE | ✅ | ✅ | Diagnostic loop-back (almost never used in APIs) |
CONNECT | ❌ | ❌ | Establish a tunnel (HTTP proxies only) |
Status code families:
| Range | Meaning | Common Codes |
|---|---|---|
| 2xx | Success | 200, 201, 204 |
| 3xx | Redirection | 301, 302, 304 |
| 4xx | Client error | 400, 401, 403, 404, 409, 422, 429 |
| 5xx | Server error | 500, 502, 503 |
Gotchas:
- ⚠️
PUTreplaces the entire resource — omitting a field deletes it - ⚠️
PATCHis not guaranteed idempotent — it depends on how you implement it - ⚠️
401means "not authenticated";403means "authenticated but not authorized" - ⚠️ Never return
200with an error body — use 4xx or 5xx
See also:
- Resource Design: URLs, Nouns, and Hierarchies — the previous article in this module
- Request & Response Design: Payloads, Headers, and Conventions (coming soon)
Version Information
Relevant specifications:
- HTTP/1.1 Methods: RFC 7231, Section 4
- PATCH method: RFC 5789
- HTTP Status Codes: RFC 7231, Section 6
- Additional Status Codes: RFC 6585 (introduces 429)
Note: HTTP/2 uses the same method and status code semantics as HTTP/1.1 — everything in this article applies equally to both versions.
Last verified: June 2025
What You Need to Know First
Required reading (in order):
- REST APIs: What They Are and How They Work — you need to understand the uniform interface and what HTTP methods mean in REST context
- Resource Design: URLs, Nouns, and Hierarchies — this article builds directly on TaskFlow's URL structure
Helpful background:
- Basic familiarity with making HTTP requests (using a browser, curl, or Postman) will make the examples feel concrete
What We'll Cover in This Article
By the end of this guide, you'll understand:
- All nine HTTP methods — what each one means and when to use it
- Safety and idempotency — what these properties are, why they matter, and which methods have them
- The complete status code landscape across all five families
- How to pick the right status code for any situation
- Common method and status code mistakes — and how to fix them
- How to apply all of this to TaskFlow's endpoints
What We'll Explain Along the Way
Don't worry if you're unfamiliar with these — we'll define them as we encounter them:
- Idempotency (formal definition with examples)
- Safety (formal definition)
- Caching and how methods affect it
- CORS preflight requests (briefly, in the
OPTIONSsection) - Content negotiation
Two Properties Every Method Has: Safety and Idempotency
Before we go through the methods themselves, we need to understand two properties that define how clients and infrastructure should treat them. These aren't optional nuances — they affect caching, retries, and the behavior of every HTTP intermediary between your client and your server.
Safety
A method is safe if it doesn't change server state. Safe methods are read-only. Calling a safe method any number of times has no side effects on the server — it's like reading a book. The book doesn't change because you read it.
Safe methods: GET, HEAD, OPTIONS, TRACE
This property matters because HTTP infrastructure — browsers, proxies, caches — treats safe methods differently. A browser will happily retry a failed GET request automatically. It won't automatically retry a failed POST, because POST might create duplicate data.
Idempotency
A method is idempotent if calling it multiple times produces the same result as calling it once. The server state after the first call is the same as after the tenth call.
Idempotent methods: GET, HEAD, OPTIONS, TRACE, PUT, DELETE
Let's make this concrete with a real example:
DELETE /tasks/task_99
First call: Task exists → deleted → server returns 204
Second call: Task is already gone → server returns 404
Third call: Same thing → 404
The result is different (204 vs 404), but the server state is identical after all three calls: task 99 doesn't exist. That's idempotency — the state converges, even if the response code varies.
Why does idempotency matter? Retries. Networks are unreliable. A client sends a request and never gets a response — was it received? If the method is idempotent, the client can safely retry. If the method is not idempotent (like POST), a retry might create a duplicate order, charge a card twice, or send the same email multiple times.
Here's a summary of both properties:
Diagram: The decision tree for safety and idempotency. Safe methods are always idempotent. Idempotent methods aren't always safe. POST and PATCH are neither.
Now let's go through every method.
The Nine HTTP Methods
GET — Retrieve a Resource
GET is the most-used method in HTTP. It retrieves a resource or collection without modifying anything.
GET /tasks — Retrieve all tasks
GET /tasks/task_99 — Retrieve task 99
GET /users/user_42 — Retrieve user 42's profile
Safe: ✅ Idempotent: ✅
GET requests must never have a request body. Filters, sorting, and pagination happen through query parameters:
GET /tasks?status=open&assignee=user_42&sort=due_date&limit=20
Caching: GET responses are cacheable by default. HTTP infrastructure (CDNs, browsers, proxies) can store and reuse them. This is one of the most powerful performance wins available to any REST API — design your GET responses to work well with Cache-Control headers, which we'll cover in the next article.
In TaskFlow: Every "load" operation — opening a task list, reading a task detail, viewing a user profile — is a GET.
POST — Create a New Resource
POST creates a new resource in a collection. The server assigns the new resource's ID.
POST /tasks
Body: { "title": "Review pull request", "assignee": "user_42" }
Response: 201 Created
Location: /tasks/task_99
Body: { "id": "task_99", "title": "Review pull request", ... }
Safe: ❌ Idempotent: ❌
Not idempotent is the key thing to remember about POST. Sending the same POST twice creates two tasks. This means:
- Clients should not automatically retry failed
POSTrequests - You should design client UIs to prevent accidental double-submission (disable the button after click, show a loading state)
- If idempotent creation matters (e.g., prevent duplicate orders), you'll need an idempotency key pattern — a client-generated ID sent in a header that the server uses to deduplicate
Beyond creation: POST is also used for operations that trigger a process without creating a persistable resource — like sending a password reset email, triggering a batch job, or initiating a payment. In TaskFlow, we use POST /projects/{projectId}/archive for the archiving action.
In TaskFlow: Creating tasks, creating projects, creating users, adding comments.
PUT — Replace a Resource Entirely
PUT replaces a resource completely. Whatever you send in the body becomes the new state of the resource. Fields you omit are deleted.
PUT /tasks/task_99
Body: {
"title": "Review pull request #204",
"assignee": "user_42",
"status": "open",
"due_date": "2025-08-01",
"project_id": "proj_5"
}
This replaces every field on task 99. If due_date wasn't in your body, it gets cleared.
Safe: ❌ Idempotent: ✅
Idempotency is the defining characteristic of PUT. Send the same body ten times — the resource ends up in exactly the same state. This makes PUT safe to retry.
PUT vs POST for creation: You can also use PUT to create a resource when the client knows the ID it wants to use:
PUT /tasks/task_custom-id-from-client
Body: { "title": "A task with a client-assigned ID" }
If the resource doesn't exist, the server creates it. If it does, the server replaces it. This pattern is used when clients generate their own IDs (UUIDs are common here).
The common mistake: Using PUT when you mean PATCH. If your client only sends the fields it wants to change — without sending the full resource — you're accidentally clearing all the other fields. Always check: am I replacing the whole thing, or just changing part of it?
In TaskFlow: PUT is used sparingly — primarily for the label-to-task assignment endpoint (PUT /tasks/{taskId}/labels/{labelId}), where the semantics are naturally "establish this relationship."
PATCH — Partially Update a Resource
PATCH modifies part of a resource. You send only the fields you want to change. Fields you omit are left untouched.
PATCH /tasks/task_99
Body: { "status": "complete" }
This updates only the status field. The task's title, assignee, due date — all untouched.
Safe: ❌ Idempotent: ❌ (in the general case)
Wait — if you're just setting a field to a value, why isn't PATCH idempotent? Because the spec allows PATCH bodies to describe instructions, not just target states:
PATCH /tasks/task_99
Body: { "comment_count": { "increment": 1 } }
Applying this twice produces a different result than applying it once. That's not idempotent. Whether your specific PATCH implementation is idempotent depends on how you implement it — simple field-setting patches (like the status example) are idempotent in practice, even if the method itself isn't guaranteed to be.
PATCH vs PUT — the definitive rule:
Need to change one or a few fields? → PATCH
Need to replace the entire resource? → PUT
Unsure? → PATCH (it's safer — you won't accidentally clear fields)
In TaskFlow: Almost every "update" operation — changing a task's status, reassigning a task, editing a comment, updating a project name — uses PATCH.
DELETE — Remove a Resource
DELETE removes a resource. After a successful delete, the resource is gone.
DELETE /tasks/task_99
Response: 204 No Content (no body)
Safe: ❌ Idempotent: ✅
The idempotency of DELETE surprises some developers. Here's the reasoning: the goal of DELETE /tasks/task_99 is that task 99 should not exist. After the first call, it doesn't. After the second call, it still doesn't. The server state is identical. The fact that the second call returns 404 instead of 204 is a response code difference, not a state difference.
Soft deletes: Many production systems don't actually delete data — they mark it as deleted (a deleted_at timestamp, a status: "deleted" field). From the API's perspective, the resource should still return 404 after deletion — the client doesn't care whether the row is gone or just flagged. The DELETE semantics are preserved.
Cascading deletes: When you delete a resource that has children, document what happens to the children. Does DELETE /tasks/task_99 also delete its comments? Or are comments orphaned? This is a business logic decision, but it should be explicit in your API documentation.
In TaskFlow: Deleting tasks, deleting projects, deleting user accounts, deleting comments. Deleting a task cascades to delete its comments.
HEAD — Retrieve Headers Without a Body
HEAD is identical to GET, except the server returns only the headers — no response body. It's useful for checking whether a resource exists, what its content type is, or when it was last modified — without the cost of transferring the full body.
HEAD /tasks/task_99
Response: 200 OK
Headers: Content-Type: application/json, Last-Modified: ..., ETag: "abc123"
(No body)
Safe: ✅ Idempotent: ✅
When clients use it:
- Checking if a large file has changed before downloading it again (compare
ETagorLast-Modified) - Verifying a resource exists without fetching its data
- Checking content type before deciding whether to proceed
You rarely design endpoints specifically for HEAD — your server framework should automatically support HEAD for any route that supports GET, returning the same headers it would for GET but without the body.
In TaskFlow: Not explicitly designed for, but supported automatically by the framework.
OPTIONS — Discover Available Methods
OPTIONS asks the server: "What methods does this resource support?" The server responds with an Allow header listing them.
OPTIONS /tasks/task_99
Response: 200 OK
Allow: GET, PATCH, DELETE, HEAD, OPTIONS
Safe: ✅ Idempotent: ✅
The more important use — CORS preflight: If you've worked with browser-based JavaScript apps calling APIs, you've almost certainly encountered OPTIONS requests appearing in your network tab. These are CORS (Cross-Origin Resource Sharing) preflight requests, automatically sent by browsers before certain cross-origin requests.
OPTIONS /tasks
Origin: https://app.taskflow.com
Access-Control-Request-Method: POST
Access-Control-Request-Headers: Authorization, Content-Type
Response: 200 OK
Access-Control-Allow-Origin: https://app.taskflow.com
Access-Control-Allow-Methods: GET, POST, PATCH, DELETE
Access-Control-Allow-Headers: Authorization, Content-Type
The browser sends this OPTIONS request to ask: "Is a cross-origin POST from app.taskflow.com allowed?" If the server says yes, the browser proceeds with the actual request. Your server must respond to OPTIONS correctly or cross-origin clients won't be able to reach your API at all.
Most web frameworks handle CORS preflight automatically with a middleware configuration — you rarely need to handle OPTIONS manually.
TRACE — Diagnostic Loop-Back
TRACE asks the server to echo back the request it received. It's a diagnostic tool to see what intermediaries (proxies, gateways) have modified along the way.
Safe: ✅ Idempotent: ✅
TRACE is almost never used in production APIs. It has security implications (it can expose sensitive headers like auth tokens to JavaScript in certain configurations), and most servers disable it. It's included here for completeness.
In TaskFlow: Disabled.
CONNECT — Establish a Tunnel
CONNECT is used by HTTP proxies to establish a tunnel — it converts the connection to a raw TCP tunnel, typically for HTTPS through an HTTP proxy. You'll never use this in a REST API.
Safe: ❌ Idempotent: ❌
In TaskFlow: Not applicable.
HTTP Status Codes: The Complete Landscape
Status codes are how the server tells the client what happened. Every response has one. Using the right code isn't just good practice — it's the difference between clients that can handle errors gracefully and clients that silently fail or retry the wrong things.
There are over 60 registered status codes. We'll cover the ones that matter for API design — which is roughly 20 — organized by family.
2xx: Success
The request was received, understood, and processed successfully.
200 OK
The most general success code. The request worked and the response body contains the result.
Use for: GET responses, PATCH responses when returning the updated resource, PUT responses when returning the replaced resource.
GET /tasks/task_99
Response: 200 OK
Body: { "id": "task_99", "title": "...", ... }
201 Created
The request succeeded and a new resource was created. Always include a Location header pointing to the new resource.
Use for: Successful POST requests that create a resource.
POST /tasks
Response: 201 Created
Location: /tasks/task_99
Body: { "id": "task_99", ... } ← Return the created resource
202 Accepted
The request was accepted but processing isn't complete yet. Used for asynchronous operations — the server is working on it but doesn't have a result yet.
Use for: Long-running operations where you can't return the result immediately (bulk imports, report generation, background jobs).
POST /projects/proj_42/archive
Response: 202 Accepted
Body: { "job_id": "job_7", "status_url": "/jobs/job_7" }
The client can poll /jobs/job_7 to check progress.
204 No Content
The request succeeded but there's nothing to return. No response body.
Use for: DELETE responses, PATCH/PUT when you choose not to return the updated resource.
DELETE /tasks/task_99
Response: 204 No Content
(No body)
3xx: Redirection
The client needs to take additional action to complete the request — usually following a redirect.
301 Moved Permanently
The resource has permanently moved to a new URL. The client should update its bookmarks and always use the new URL going forward.
Use for: API URL changes during versioning or restructuring. Clients and caches should permanently switch to the new URL.
302 Found
The resource is temporarily at a different URL. The client should use the new URL for this request but continue using the original URL for future requests.
Use for: Temporary redirects during maintenance or A/B testing.
304 Not Modified
The client's cached version is still current. The server is telling the client: "The resource hasn't changed since you last fetched it — use what you have."
Use for: Cache validation responses (in conjunction with ETag or Last-Modified headers). This is what makes conditional GET requests efficient.
GET /tasks/task_99
If-None-Match: "etag-abc123"
Response: 304 Not Modified ← No body — client uses cached version
4xx: Client Errors
The client made a mistake. The request was understood but can't be fulfilled as-is. The client needs to fix the request before retrying.
This is a critical distinction from 5xx: 4xx means "your request was wrong — fix it." 5xx means "we broke — try again later."
400 Bad Request
The request was malformed, syntactically invalid, or missing required fields. The most general client error.
POST /tasks
Body: { "due_date": "not-a-date" }
Response: 400 Bad Request
Body: {
"error": "validation_error",
"message": "due_date must be a valid ISO 8601 date",
"field": "due_date"
}
401 Unauthorized
Despite the name, this means unauthenticated — "I don't know who you are." The client should provide or refresh credentials before retrying.
GET /tasks
(No Authorization header)
Response: 401 Unauthorized
WWW-Authenticate: Bearer realm="taskflow"
Body: { "error": "authentication_required", "message": "Provide a valid Bearer token." }
The WWW-Authenticate header tells the client what kind of credentials to provide.
403 Forbidden
The client is authenticated (we know who they are) but not authorized (they don't have permission). Unlike 401, sending credentials again won't help.
DELETE /users/user_99
Authorization: Bearer <user_42's token>
Response: 403 Forbidden
Body: { "error": "insufficient_permissions", "message": "You cannot delete another user's account." }
The 401 vs 403 rule:
- Not logged in at all? →
401 - Logged in but not allowed? →
403
404 Not Found
The resource doesn't exist at the requested URL. This applies to both "this resource never existed" and "this resource existed but was deleted."
GET /tasks/task_nonexistent
Response: 404 Not Found
Body: { "error": "not_found", "message": "Task 'task_nonexistent' does not exist." }
Note: Some APIs return 404 when a resource exists but the current user isn't allowed to see it — to avoid leaking information about whether a resource exists. This is a security pattern, not standard behavior.
405 Method Not Allowed
The HTTP method used isn't supported for this resource. Always include an Allow header listing what methods are permitted.
DELETE /users ← Deleting the entire user collection isn't allowed
Response: 405 Method Not Allowed
Allow: GET, POST
409 Conflict
The request conflicts with the current state of the resource. The client needs to resolve the conflict before retrying.
POST /users
Body: { "email": "alex@example.com" }
Response: 409 Conflict
Body: { "error": "duplicate_email", "message": "A user with this email already exists." }
Also used for optimistic concurrency conflicts — when a client tries to update a resource that's been modified by someone else since they last fetched it.
410 Gone
The resource existed but has been permanently deleted and won't come back. Unlike 404, 410 signals that the client should stop asking for this resource.
Use for: deleted user accounts, expired content, deprecated API endpoints.
422 Unprocessable Entity
The request body is syntactically valid (parseable JSON) but semantically invalid — the values don't make sense in context.
POST /tasks
Body: { "due_date": "2020-01-01" } ← Valid JSON, valid date format, but in the past
Response: 422 Unprocessable Entity
Body: {
"error": "validation_error",
"message": "due_date cannot be in the past",
"field": "due_date"
}
400 vs 422:
- Body can't be parsed at all (malformed JSON, wrong content type)? →
400 - Body is valid JSON but the values fail business rules? →
422
Many APIs use 400 for both. Either is acceptable as long as you're consistent and the error response body provides enough detail for the client to understand and fix the problem.
429 Too Many Requests
The client has sent too many requests in a given time window (rate limiting). Always include a Retry-After header indicating when the client can try again.
GET /tasks
Authorization: Bearer <token>
Response: 429 Too Many Requests
Retry-After: 30
X-RateLimit-Limit: 100
X-RateLimit-Remaining: 0
X-RateLimit-Reset: 1719432000
Body: { "error": "rate_limited", "message": "Too many requests. Retry after 30 seconds." }
5xx: Server Errors
The server failed. The request may have been valid, but something went wrong on the server's side. The client can typically retry 5xx responses (with backoff) without modifying the request.
500 Internal Server Error
An unexpected condition was encountered. The catch-all server error — "something broke and we're not sure what."
This is what you return when an uncaught exception occurs. Always log the details server-side. Never send a stack trace to the client.
GET /tasks/task_99
Response: 500 Internal Server Error
Body: { "error": "internal_error", "message": "An unexpected error occurred. Please try again." }
502 Bad Gateway
The server received an invalid response from an upstream service (a database, another microservice, a third-party API). The server itself is up, but something behind it broke.
503 Service Unavailable
The server is temporarily unable to handle the request — overloaded, starting up, or down for maintenance. Include a Retry-After header when possible.
504 Gateway Timeout
The server didn't receive a timely response from an upstream service. Similar to 502 but specifically a timeout.
The Status Code Decision Tree
Let's put this together into a practical decision framework for every response your TaskFlow API might return.
Diagram: A decision tree for selecting the right HTTP status code. Start at the top: did the request succeed? Follow the branches based on the failure mode.
Applying Methods and Status Codes to TaskFlow
Let's map every endpoint in TaskFlow to its correct method and expected responses.
Tasks Endpoints
// List all tasks
GET /tasks?status=open&assignee=user_42
→ 200 OK (tasks found, returns array — possibly empty)
// Create a task
POST /tasks
Body: { "title": "...", "project_id": "proj_5" }
→ 201 Created (success, returns created task + Location header)
→ 400 Bad Request (missing required "title" field)
→ 422 Unprocessable Entity (due_date in the past)
// Get a single task
GET /tasks/task_99
→ 200 OK (task exists)
→ 404 Not Found (task doesn't exist)
// Update a task partially
PATCH /tasks/task_99
Body: { "status": "complete" }
→ 200 OK (success, returns updated task)
→ 404 Not Found (task doesn't exist)
→ 422 (invalid status value)
// Delete a task
DELETE /tasks/task_99
→ 204 No Content (deleted)
→ 404 Not Found (already gone — or never existed)
Users Endpoints
// Create a user (registration)
POST /users
Body: { "email": "alex@example.com", "name": "Alex" }
→ 201 Created (registered)
→ 409 Conflict (email already in use)
→ 422 (invalid email format)
// Get a user profile
GET /users/user_42
→ 200 OK (user exists and requester is authorized to see them)
→ 404 Not Found (user doesn't exist)
// Update a user (authenticated user updating their own profile)
PATCH /users/user_42
Body: { "name": "Alex Johnson" }
→ 200 OK (updated)
→ 403 Forbidden (trying to update someone else's profile)
Comments Endpoints
// List comments on a task
GET /tasks/task_99/comments
→ 200 OK (always — empty array if no comments)
→ 404 Not Found (if the parent task doesn't exist)
// Add a comment
POST /tasks/task_99/comments
Body: { "body": "Looks good, I'll review today." }
→ 201 Created (comment created)
→ 404 Not Found (parent task doesn't exist)
→ 400 Bad Request (empty body)
// Edit a comment
PATCH /tasks/task_99/comments/comment_7
Body: { "body": "Actually, I'll review tomorrow." }
→ 200 OK (updated)
→ 403 Forbidden (trying to edit someone else's comment)
→ 404 Not Found (comment doesn't exist)
// Delete a comment
DELETE /tasks/task_99/comments/comment_7
→ 204 No Content (deleted)
→ 403 Forbidden (not the comment author or an admin)
→ 404 Not Found (comment doesn't exist)
Common Mistakes
❌ Returning 200 with an Error Body
This is the most common mistake in API design, and it's surprisingly hard to debug.
// ❌ Wrong
GET /tasks/task_99
Response: 200 OK
Body: { "error": true, "message": "Task not found" }
// ✅ Correct
Response: 404 Not Found
Body: { "error": "not_found", "message": "Task not found" }
Why is 200 with an error body bad? Because every HTTP client, framework, and monitoring tool treats 2xx as success. Your logging won't catch it as an error. Your client-side fetch won't reject the promise. Your uptime monitor won't alert. The error is invisible.
Rule: The status code is the primary signal. The body provides detail.
❌ Using 404 When It Should Be 400
// ❌ Wrong — "Task not found" for a missing field
POST /tasks
Body: {} ← Missing "title"
Response: 404 Not Found
Body: { "error": "title field is required" }
// ✅ Correct
Response: 400 Bad Request
Body: { "error": "validation_error", "field": "title", "message": "title is required" }
404 means a resource doesn't exist at a URL. A validation error has nothing to do with URL routing — the problem is in the request body. Use 400 or 422.
❌ Using POST for Everything
// ❌ Wrong — using POST for reads
POST /getTasks
// ❌ Wrong — using POST for deletes
POST /deleteTask
Body: { "id": "task_99" }
// ✅ Correct
GET /tasks
DELETE /tasks/task_99
Using POST for reads breaks caching. Using POST for deletes breaks idempotency and signals the wrong intent to every infrastructure layer.
❌ Confusing PUT and PATCH
// Scenario: task_99 has title, assignee, status, and due_date.
// You want to change only the status to "complete".
// ❌ Wrong — using PUT with partial body
PUT /tasks/task_99
Body: { "status": "complete" }
// Result: title, assignee, and due_date are now DELETED
// ✅ Correct — use PATCH for partial updates
PATCH /tasks/task_99
Body: { "status": "complete" }
// Result: only status changes; other fields untouched
❌ Returning 403 When the User Isn't Logged In
// ❌ Wrong — using 403 for unauthenticated requests
GET /tasks
(No Authorization header)
Response: 403 Forbidden
// ✅ Correct
Response: 401 Unauthorized
WWW-Authenticate: Bearer
403 tells the client "you're authenticated but not allowed." A client that isn't logged in shouldn't interpret that as "log in as someone else" — it should interpret it as "your credentials are wrong." 401 sends the right signal.
Common Misconceptions
❌ Misconception: PATCH is always idempotent
Reality: PATCH is defined as not necessarily idempotent per RFC 5789. Whether a specific PATCH implementation is idempotent depends entirely on what the patch body contains and how the server applies it.
Setting a field to a value ({ "status": "complete" }) is idempotent in practice. Incrementing a counter ({ "view_count": { "increment": 1 } }) is not.
Why this matters: Don't configure your HTTP infrastructure to auto-retry failed PATCH requests unless you've verified your specific patch operations are safe to retry.
❌ Misconception: DELETE should return 404 if the resource is already gone
Reality: Returning 404 on a second DELETE is valid (and common), but returning 204 is equally valid. The idempotency of DELETE refers to the server state, not the response code. Both approaches are correct REST. Pick one and be consistent.
Some teams prefer returning 204 always (including for already-deleted resources) because it's simpler for clients: a DELETE always succeeds, the client doesn't need to handle 404. Other teams prefer 404 to distinguish "I deleted it now" from "it was already gone." Document your choice.
❌ Misconception: 500 means the server is down
Reality: 500 means an unexpected error occurred. The server is running — it received and processed your request — but something went wrong during processing. A server that's actually unreachable returns no response at all (connection refused, timeout), not a 500.
❌ Misconception: You should use the most specific status code possible, always
Reality: More specific codes are better when they help the client take a meaningful action. But obscure codes (451 Unavailable for Legal Reasons, 507 Insufficient Storage) add confusion when clients don't know how to handle them. Use specific codes when specificity helps. Fall back to the general code (400, 500) when the specific code would confuse more than clarify.
Troubleshooting Common Issues
Problem: Clients are treating errors as successes
Symptoms: Error responses appear in success logs; client UI shows no error message when something fails.
Cause: Almost always a 200 response with an error in the body.
Fix: Audit every endpoint and verify that error cases return the correct 4xx or 5xx code. Add an integration test that asserts status codes, not just body content.
// Integration test example
const response = await fetch("/tasks/nonexistent");
expect(response.status).toBe(404); // ← Test the status code explicitly
Problem: Browser requests are failing with CORS errors before even reaching the API
Symptoms: Preflight OPTIONS requests return 405 Method Not Allowed or no CORS headers.
Fix: Ensure your server responds to OPTIONS for every route with the correct Access-Control-Allow-* headers. Most frameworks handle this with a CORS middleware — configure it to allow the methods and headers your API uses.
// Express example
import cors from "cors";
app.use(
cors({
origin: "https://app.taskflow.com",
methods: ["GET", "POST", "PATCH", "PUT", "DELETE", "OPTIONS"],
allowedHeaders: ["Authorization", "Content-Type"],
}),
);
Problem: "Should I use 400 or 422 for validation errors?"
Resolution: Either is acceptable. The meaningful thing is the error response body, not the distinction between 400 and 422. Pick one for your API and apply it consistently:
- Use
400for both malformed requests and validation failures (simpler, more common) - Use
422specifically for "syntactically valid but semantically wrong" (more precise)
Document your convention. What matters is that clients get enough information in the response body to fix the problem:
// A good validation error body — regardless of 400 or 422
{
"error": "validation_error",
"message": "The request contains invalid fields.",
"details": [
{ "field": "due_date", "message": "Must be a future date." },
{ "field": "title", "message": "Cannot be empty." }
]
}
Check Your Understanding
Quick Quiz
-
A client sends
DELETE /tasks/task_99and the task is already deleted. What status code should the server return?Show Answer
Either
204 No Contentor404 Not Found— both are correct.404is more informative ("it wasn't there"),204is simpler for clients ("delete always succeeds"). Document your choice and apply it consistently. -
What's wrong with this response?
POST /tasks
Body: {}
Response: 200 OK
Body: { "success": false, "error": "title is required" }Show Answer
Two problems:
200 OKshould not be used for errors — use400 Bad Requestor422- The response body uses
"success": falseinstead of a standard error structure
Correct response:
400 Bad Request
Body: { "error": "validation_error", "field": "title", "message": "title is required" } -
A user is logged in but tries to delete another user's comment. What status code should the server return?
Show Answer
403 Forbidden. The user is authenticated (we know who they are), but they don't have permission to delete someone else's comment.401would be wrong here —401means unauthenticated. -
Why can
GETrequests be automatically retried by HTTP clients and intermediaries, butPOSTrequests cannot?Show Answer
GETis both safe and idempotent — it doesn't change server state and produces the same result every time. Retrying a failedGETis harmless.POSTis neither safe nor idempotent — sending the samePOSTtwice can create two resources (two tasks, two orders, two charges). Automatic retries would cause duplicates.
Hands-On Exercise
Challenge: For each of the following scenarios, specify the correct HTTP method and status code. Then write the response body structure.
- A user submits a registration form. The email they entered is already in use.
- An admin requests the list of all users — the list is empty.
- A client tries to update a task that was deleted 5 minutes ago.
- A client sends a request with a valid auth token to update another user's profile.
- The TaskFlow database goes down. A client requests a task.
Show Answer
-
Email already in use:
Method: POST /users
Status: 409 Conflict
Body: {
"error": "duplicate_email",
"message": "An account with this email already exists."
} -
Empty user list:
Method: GET /users
Status: 200 OK
Body: { "data": [], "total": 0 }An empty collection is a successful response — not a 404. The resource (the
/userscollection) exists; it just has no items. -
Update a deleted task:
Method: PATCH /tasks/task_deleted
Status: 404 Not Found
Body: {
"error": "not_found",
"message": "Task 'task_deleted' does not exist."
} -
Updating another user's profile:
Method: PATCH /users/user_99
Status: 403 Forbidden
Body: {
"error": "insufficient_permissions",
"message": "You can only update your own profile."
} -
Database is down:
Method: GET /tasks/task_99
Status: 503 Service Unavailable
Retry-After: 10
Body: {
"error": "service_unavailable",
"message": "TaskFlow is temporarily unavailable. Please try again shortly."
}
Summary: Key Takeaways
-
Nine HTTP methods exist, but five handle the vast majority of REST API operations:
GET(read),POST(create),PUT(replace),PATCH(partial update), andDELETE(remove). -
Safety means read-only.
GET,HEAD,OPTIONS, andTRACEare safe — they don't change server state. Safe methods can be cached, automatically retried, and logged differently. -
Idempotency means calling multiple times equals calling once.
GET,HEAD,OPTIONS,PUT, andDELETEare idempotent.POSTandPATCHare not. Idempotency determines whether a client can safely retry a request. -
PUTreplaces the whole resource;PATCHupdates part of it. Confusing them accidentally deletes fields. When in doubt, usePATCH. -
Status codes communicate outcome semantics. Use 2xx for success, 4xx for client errors ("fix your request"), and 5xx for server errors ("try again later"). Never return 2xx for an error.
-
The 401/403 distinction matters:
401= "we don't know who you are — authenticate first."403= "we know who you are — you're not allowed." -
TaskFlow's 20 endpoints each map to a specific method and a small set of expected status codes. That mapping is now explicit, documented, and consistent.
What's Next?
You now have the complete vocabulary of HTTP: the nouns (URLs and resources from Article 2), the verbs (HTTP methods), and the outcomes (status codes).
The natural next step is Request & Response Design: Payloads, Headers, and Conventions (coming soon) — where we shape what actually travels over the wire. You'll design the JSON body structure for every TaskFlow request and response, choose pagination and filtering patterns, define a consistent error format, and learn which HTTP headers matter most for a production API.
References
- RFC 7231 — HTTP/1.1 Semantics and Content — IETF specification formally defining all HTTP methods (Section 4) and status codes (Section 6), including the safe and idempotent classifications used throughout this article.
- RFC 5789 — PATCH Method for HTTP — The formal definition of the PATCH method, including the clarification that PATCH is not guaranteed to be idempotent.
- RFC 6585 — Additional HTTP Status Codes — Introduces status code 429 (Too Many Requests), used in the rate limiting section.
- HTTP response status codes — MDN Web Docs — Comprehensive reference for all HTTP status codes with usage guidance. Used as a cross-reference for status code descriptions and examples.
- HTTP request methods — MDN Web Docs — MDN reference for HTTP methods, cross-referenced for the HEAD and OPTIONS sections.