Loading content...
Every HTTP response begins with a three-digit status code that tells the client what happened. These codes aren't arbitrary numbers—they're a standardized vocabulary that enables clients, proxies, and infrastructure to understand and react to outcomes without parsing response bodies.
When your API uses status codes correctly, magical things happen: browsers know when to show login pages, CDNs know what to cache, retry logic knows when to try again, and developers can diagnose issues at a glance. When you misuse them—returning 200 for errors, or 400 for server failures—you break this ecosystem of understanding.
In this page, we'll explore HTTP status codes systematically, understand when to use each, design clear response bodies for both success and error cases, and learn the patterns that make APIs easy to consume and debug.
By the end of this page, you will understand the five classes of HTTP status codes, know which specific codes to use for common scenarios, and be able to design consistent response formats for success and error cases. You'll master the art of communicating outcomes clearly through the standard HTTP vocabulary.
HTTP status codes are organized into five categories, each with distinct meaning:
The request was received, processing continues. Rarely used directly in REST APIs.
The request was successfully received, understood, and accepted.
Further action needed to complete the request.
The request contains an error attributable to the client.
The server failed to fulfill a valid request.
Even if you don't recognize a specific code, the first digit tells you the category. A 418 is still a 4xx client error; a 507 is still a 5xx server error. Clients should handle unknown codes based on their category.
Success codes confirm that the server understood and fulfilled the request. Choosing the right success code communicates important details about how the request succeeded.
| Code | Name | When to Use | Response Body |
|---|---|---|---|
| 200 | OK | Standard success for GET, PUT, PATCH | Resource representation |
| 201 | Created | New resource created (usually POST) | Created resource + Location header |
| 202 | Accepted | Request queued for async processing | Status info, job ID, or nothing |
| 204 | No Content | Success but nothing to return (DELETE, some PUT) | None (no body allowed) |
| 206 | Partial Content | Range request fulfilled (file downloads) | Requested byte range |
12345678910111213141516171819202122232425262728293031323334353637383940414243444546
# 200 OK - Standard successful GETGET /api/users/123 HTTP/1.1 HTTP/1.1 200 OKContent-Type: application/json { "id": 123, "name": "Alice Thompson", "email": "alice@example.com"} # 201 Created - New resourcePOST /api/users HTTP/1.1Content-Type: application/json {"name": "Bob Wilson", "email": "bob@example.com"} HTTP/1.1 201 CreatedContent-Type: application/jsonLocation: /api/users/456 { "id": 456, "name": "Bob Wilson", "email": "bob@example.com", "createdAt": "2024-06-15T10:00:00Z"} # 202 Accepted - Asynchronous operationPOST /api/reports/generate HTTP/1.1 HTTP/1.1 202 AcceptedContent-Type: application/json { "jobId": "job-789", "status": "processing", "estimatedTime": "30 seconds", "statusUrl": "/api/reports/jobs/job-789"} # 204 No Content - Successful DELETEDELETE /api/users/123 HTTP/1.1 HTTP/1.1 204 No ContentUse 200 for GET, PUT, and PATCH that return data. Use 201 for POST (or PUT) that creates a new resource—always include Location header. Use 204 when the operation succeeds but there's nothing to return (common for DELETE). Don't default to 200 for everything—specific codes provide valuable information.
4xx codes indicate that the client made an error. The request should not be retried without modification. These codes are crucial for guiding clients to fix their requests.
| Code | Name | Meaning | Common Scenarios |
|---|---|---|---|
| 400 | Bad Request | Malformed request syntax | Invalid JSON, missing required fields, wrong data types |
| 401 | Unauthorized | Authentication required or failed | Missing token, expired token, invalid credentials |
| 403 | Forbidden | Authenticated but not authorized | Insufficient permissions, restricted resource |
| 404 | Not Found | Resource doesn't exist | Invalid ID, deleted resource, wrong endpoint |
| 405 | Method Not Allowed | HTTP method not supported | POST to a read-only endpoint |
| 406 | Not Acceptable | Can't produce requested format | Accept: text/plain when only JSON available |
| 409 | Conflict | Conflicts with current state | Duplicate key, version mismatch, concurrent edit |
| 415 | Unsupported Media Type | Request body format not supported | Sending XML when only JSON accepted |
| 422 | Unprocessable Entity | Syntactically valid but semantically wrong | Valid JSON but business rule violated |
| 429 | Too Many Requests | Rate limit exceeded | Too many API calls in time window |
Both indicate client input errors, but there's a subtle distinction:
400 Bad Request: The request syntax is malformed. The server can't even parse it.
{name: "Alice"} (unquoted key)422 Unprocessable Entity: The request is syntactically valid but violates business rules.
{"email": "not-an-email"}{"age": -5}Practical advice: Many APIs use 400 for all validation errors, which is acceptable. If you want finer granularity, 422 signals "I understood your request, but can't process it due to semantic issues."
12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182
# 400 Bad Request - Malformed JSONPOST /api/users HTTP/1.1Content-Type: application/json {name: "Invalid JSON"} HTTP/1.1 400 Bad RequestContent-Type: application/json { "error": "bad_request", "message": "Request body is not valid JSON", "details": "Unexpected token 'n' at position 1"} # 401 Unauthorized - Missing authenticationGET /api/users/123 HTTP/1.1 HTTP/1.1 401 UnauthorizedWWW-Authenticate: Bearer realm="api"Content-Type: application/json { "error": "unauthorized", "message": "Authentication required. Please provide a valid access token."} # 403 Forbidden - Insufficient permissionsDELETE /api/admin/users/123 HTTP/1.1Authorization: Bearer user-token HTTP/1.1 403 ForbiddenContent-Type: application/json { "error": "forbidden", "message": "You do not have permission to delete users", "requiredRole": "admin", "currentRole": "user"} # 404 Not Found - Resource doesn't existGET /api/users/99999 HTTP/1.1 HTTP/1.1 404 Not FoundContent-Type: application/json { "error": "not_found", "message": "User with ID 99999 not found", "resource": "user", "id": "99999"} # 409 Conflict - Duplicate or version conflictPUT /api/users/123 HTTP/1.1If-Match: "old-etag" HTTP/1.1 409 ConflictContent-Type: application/json { "error": "conflict", "message": "Resource was modified by another request", "currentVersion": "v2.0", "yourVersion": "v1.0"} # 429 Too Many Requests - Rate limitedGET /api/search HTTP/1.1 HTTP/1.1 429 Too Many RequestsRetry-After: 60X-RateLimit-Limit: 100X-RateLimit-Remaining: 0X-RateLimit-Reset: 1718459400 { "error": "rate_limit_exceeded", "message": "Too many requests. Please wait before retrying.", "retryAfter": 60}This is frequently confused. 401 Unauthorized means 'Who are you? Please authenticate.' 403 Forbidden means 'I know who you are, but you can't do this.' Use 401 when there's no token or the token is invalid/expired. Use 403 when the token is valid but the user lacks permission.
5xx codes indicate that the server failed to fulfill a valid request. The problem is on the server side—the client did nothing wrong. These errors are often transient and may be retried.
| Code | Name | Meaning | Retry? |
|---|---|---|---|
| 500 | Internal Server Error | Generic server error (unhandled exception) | Maybe (after delay) |
| 501 | Not Implemented | Method/feature not implemented | No |
| 502 | Bad Gateway | Invalid response from upstream service | Yes (after delay) |
| 503 | Service Unavailable | Server temporarily overloaded/maintenance | Yes (see Retry-After) |
| 504 | Gateway Timeout | Upstream service didn't respond in time | Yes (after delay) |
12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849
# 500 Internal Server ErrorGET /api/users/123 HTTP/1.1 HTTP/1.1 500 Internal Server ErrorContent-Type: application/json { "error": "internal_server_error", "message": "An unexpected error occurred. Please try again later.", "requestId": "req-abc-123-def", "support": "Contact support@example.com with the request ID"} # 502 Bad Gateway - Upstream service failureGET /api/weather HTTP/1.1 HTTP/1.1 502 Bad GatewayContent-Type: application/json { "error": "bad_gateway", "message": "Weather service is temporarily unavailable", "retryAfter": 30} # 503 Service Unavailable - Maintenance or overloadGET /api/users HTTP/1.1 HTTP/1.1 503 Service UnavailableRetry-After: 3600Content-Type: application/json { "error": "service_unavailable", "message": "Service is undergoing scheduled maintenance", "maintenanceEnd": "2024-06-15T18:00:00Z", "status": "https://status.example.com"} # 504 Gateway TimeoutGET /api/reports/complex HTTP/1.1 HTTP/1.1 504 Gateway TimeoutContent-Type: application/json { "error": "gateway_timeout", "message": "Request timed out. Try a simpler query or retry later."}Always include a unique request ID in error responses (and headers). This ID correlates with server logs, making debugging much easier. Clients can report issues by providing this ID, and you can quickly find the exact request in logs.
Never expose internal details in production:
❌ Bad:
{
"error": "NullPointerException in UserService.java line 245",
"stackTrace": "at com.example.UserService.getUser..."
}
✅ Good:
{
"error": "internal_server_error",
"message": "An unexpected error occurred",
"requestId": "req-abc-123"
}
Stack traces and internal paths reveal implementation details that could aid attackers. Log full details server-side; return only safe information to clients.
Redirection codes tell the client to look elsewhere for the requested resource. They're essential for URL migration, load balancing, and caching.
| Code | Name | Behavior | Use Case |
|---|---|---|---|
| 301 | Moved Permanently | Resource permanently at new URI; cache forever | URL restructuring, domain change |
| 302 | Found | Temporary redirect (historically ambiguous method) | Avoid; use 307 instead |
| 303 | See Other | Redirect to different resource with GET | POST/PUT result available at another URI |
| 304 | Not Modified | Cached version is valid; no body | Conditional GET caching |
| 307 | Temporary Redirect | Temporary; preserve method and body | Temporary maintenance redirect |
| 308 | Permanent Redirect | Permanent; preserve method and body | Permanent redirect for non-GET |
123456789101112131415161718192021222324252627
# 301 Moved Permanently - Old URL structureGET /api/v1/customers/123 HTTP/1.1 HTTP/1.1 301 Moved PermanentlyLocation: /api/v2/users/123 # 304 Not Modified - CachingGET /api/users/123 HTTP/1.1If-None-Match: "abc123" HTTP/1.1 304 Not ModifiedETag: "abc123" # 307 Temporary Redirect - MaintenancePOST /api/orders HTTP/1.1Content-Type: application/json {"product": "laptop"} HTTP/1.1 307 Temporary RedirectLocation: /api/backup/orders # 303 See Other - After POST, redirect to resultPOST /api/orders HTTP/1.1 HTTP/1.1 303 See OtherLocation: /api/orders/789Historical ambiguity: 301/302 were supposed to preserve the HTTP method, but browsers changed POST to GET. 307 and 308 were introduced to guarantee method preservation. Use 307 for temporary redirects that must preserve the method; use 308 for permanent redirects that must preserve the method.
Status codes communicate the outcome, but response bodies provide the details. A well-designed response format is consistent, informative, and parseable.
For successful operations, return the resource representation directly or wrapped in a consistent envelope:
12345678910111213
// Single resource - direct{ "id": 123, "name": "Alice", "email": "alice@example.com", "createdAt": "2024-01-15T10:00:00Z"} // Collection - direct array[ {"id": 1, "name": "Alice"}, {"id": 2, "name": "Bob"}]123456789101112131415161718192021222324
// Single resource - enveloped{ "data": { "id": 123, "name": "Alice" }, "meta": { "requestId": "req-abc" }} // Collection - with pagination{ "data": [ {"id": 1, "name": "Alice"}, {"id": 2, "name": "Bob"} ], "pagination": { "page": 1, "perPage": 20, "total": 150, "totalPages": 8 }}Direct vs. Envelope: Which to choose?
Most APIs use envelopes for collections (to include pagination) and direct responses for single resources. Whatever you choose, be consistent across your entire API.
Error responses should be informative, consistent, and helpful for debugging. A good error response includes:
123456789101112131415161718192021
{ "error": { "code": "validation_error", "message": "The request body contains invalid data", "details": [ { "field": "email", "code": "invalid_format", "message": "Must be a valid email address" }, { "field": "age", "code": "out_of_range", "message": "Must be between 0 and 150" } ], "requestId": "req-abc-123-def", "timestamp": "2024-06-15T10:30:00Z", "documentation": "https://api.example.com/docs/errors/validation_error" }}validation_error, not just a messageCollections can be large—you can't return all 1 million users in a single response. Pagination breaks collections into manageable pages. There are three main pagination strategies:
GET /users?offset=20&limit=10
Pros: Simple, allows jumping to any page Cons: Inconsistent results if data changes; O(n) database performance for large offsets
GET /users?page=3&per_page=10
Pros: Very intuitive for users Cons: Same consistency and performance issues as offset/limit
GET /users?cursor=eyJpZCI6MTIzfQ&limit=10
Pros: Consistent results, efficient for large datasets Cons: Can't jump to arbitrary pages; cursor is opaque
12345678910111213141516171819
{ "data": [ {"id": 21, "name": "User 21"}, {"id": 22, "name": "User 22"} ], "pagination": { "page": 3, "perPage": 10, "total": 150, "totalPages": 15 }, "links": { "self": "/users?page=3&per_page=10", "first": "/users?page=1&per_page=10", "prev": "/users?page=2&per_page=10", "next": "/users?page=4&per_page=10", "last": "/users?page=15&per_page=10" }}For large or frequently-changing datasets, cursor-based pagination is strongly preferred. It's what Twitter, Slack, and Facebook use. The cursor encodes the position in the dataset, enabling consistent traversal even as new items are added.
Response headers carry crucial metadata that clients use for caching, debugging, and controlling behavior.
| Header | Purpose | Example |
|---|---|---|
| Content-Type | Body format | application/json; charset=utf-8 |
| Location | URI of created/redirected resource | /users/456 |
| ETag | Resource version for caching | "abc123" |
| Last-Modified | When resource last changed | Tue, 15 Jun 2024 10:00:00 GMT |
| Cache-Control | Caching directives | max-age=3600, public |
| Retry-After | When to retry (for 429/503) | 60 or Tue, 15 Jun 2024 11:00:00 GMT |
| X-Request-Id | Unique request identifier | req-abc-123-def |
| **X-RateLimit-*` | Rate limit status | X-RateLimit-Remaining: 99 |
| Link | Related resources (RFC 8288) | </users?page=2>; rel="next" |
1234567891011121314151617181920
# Full response with best practice headersHTTP/1.1 200 OKContent-Type: application/json; charset=utf-8ETag: "33a64df551425fcc55e4d42a148795d9f25f89d4"Last-Modified: Sat, 15 Jun 2024 10:00:00 GMTCache-Control: private, max-age=3600X-Request-Id: req-abc-123-defX-RateLimit-Limit: 1000X-RateLimit-Remaining: 998X-RateLimit-Reset: 1718464800 { "id": 123, "name": "Alice"} # Rate limit headers explained:# X-RateLimit-Limit: Maximum requests allowed in window# X-RateLimit-Remaining: Requests remaining in current window# X-RateLimit-Reset: Unix timestamp when limit resetsAvoid these common mistakes that confuse clients and break tooling:
{"status": 200, "error": "User not found"} breaks standard error handling[] is correct| Wrong | Problem | Correct |
|---|---|---|
200 with {"error": "not found"} | Says success but returns error | 404 with error body |
500 for invalid input | Blames server for client error | 400 or 422 |
404 for forbidden resource | Says 'doesn't exist' when it does | 403 (or 404 if hiding existence) |
200 for async job start | Says completed when only accepted | 202 Accepted |
201 without Location header | Missing URI of created resource | 201 with Location header |
Status codes and response design are the communication layer of your API. Using them correctly makes your API predictable, debuggable, and compatible with the HTTP ecosystem.
Module Complete:
Congratulations! You've completed the RESTful API Design Principles module. You now understand REST as an architectural style, can model resources with proper URIs, know how to use HTTP methods correctly, and can communicate outcomes through appropriate status codes and response design.
These principles form the foundation for building web APIs that are intuitive, scalable, and maintainable. Apply them consistently, and your APIs will be a pleasure to consume.
You have mastered RESTful API Design Principles—from REST fundamentals and resource naming, through HTTP method semantics, to status codes and response design. These skills enable you to build APIs that follow industry best practices and integrate seamlessly with the HTTP ecosystem.