RESTful API Status Code Principles
RESTful APIs should use HTTP status codes to communicate operation results clearly. Proper status code usage enables:
- Automated client handling of different outcomes
- Predictable API behavior across operations
- Clear distinction between client and server errors
- Semantic meaning that goes beyond response body
- Standards compliance that clients expect
This guide covers RESTful best practices for HTTP status codes.
Core RESTful Status Code Rules
2xx Success - Operation Succeeded
200 OK: Successful request with response body
GET /users/123
200 OK
{"id": 123, "name": "John", "email": "[email protected]"}
PUT /users/123
200 OK
{"id": 123, "name": "John Updated", "email": "[email protected]"}
201 Created: Successful resource creation
POST /users
201 Created
Location: /users/124
{"id": 124, "name": "Jane", "email": "[email protected]"}
Key: Include Location header pointing to new resource
202 Accepted: Request accepted for asynchronous processing
POST /reports/generate
202 Accepted
Location: /reports/jobs/789
{"status": "processing", "job_id": "789"}
204 No Content: Successful request with no response body
DELETE /users/123
204 No Content
PUT /settings
204 No Content
Rule: Use appropriate 2xx code to clearly signal success type.
3xx Redirection - Client Action Required
301 Moved Permanently: Resource permanently relocated
GET /api/v1/users
301 Moved Permanently
Location: /api/v2/users
[Client should update to use new URL]
302 Found: Temporary redirect
GET /users
302 Found
Location: /api/users
[Client may update to new location]
304 Not Modified: Resource unchanged since last request
GET /data HTTP/1.1
If-None-Match: "abc123"
304 Not Modified
[Client uses cached copy]
307 Temporary Redirect: Like 302, but preserves HTTP method
POST /form
307 Temporary Redirect
Location: /form-processor
[Browser re-POSTs to new location, not GET]
Rule: Use 3xx when client action needed to complete request.
4xx Client Error - Client's Fault
400 Bad Request: Malformed request
POST /users
400 Bad Request
{"error": "Invalid JSON: missing closing brace"}
401 Unauthorized: Missing or invalid authentication
GET /api/admin
401 Unauthorized
{"error": "Missing authentication token"}
403 Forbidden: Authenticated but unauthorized
GET /api/admin
403 Forbidden
{"error": "User role 'viewer' lacks access to admin"}
404 Not Found: Resource doesn't exist
GET /users/999999
404 Not Found
{"error": "User not found"}
409 Conflict: Request conflicts with current state
PUT /document/123
409 Conflict
{"error": "Document was modified; refresh and retry"}
422 Unprocessable Entity: Valid format, invalid content
POST /users
422 Unprocessable Entity
{"error": "Email already in use"}
429 Too Many Requests: Rate limit exceeded
GET /api/search
429 Too Many Requests
Retry-After: 60
{"error": "Rate limit exceeded"}
Rule: 4xx indicates client made a mistake; client must correct and retry.
5xx Server Error - Server's Fault
500 Internal Server Error: Unexpected server error
GET /data
500 Internal Server Error
{"error": "Internal error occurred"}
502 Bad Gateway: Upstream service returned invalid response
GET /api/data
502 Bad Gateway
{"error": "Upstream service error"}
503 Service Unavailable: Server temporarily unavailable
GET /api/data
503 Service Unavailable
Retry-After: 300
{"error": "Server maintenance in progress"}
504 Gateway Timeout: Upstream service too slow
GET /api/data
504 Gateway Timeout
{"error": "Upstream service timeout"}
Rule: 5xx indicates server error; client might retry later.
RESTful Operation Patterns
GET - Reading Resources
GET /users → 200 OK (list of users)
GET /users/123 → 200 OK (single user)
GET /users/999 → 404 Not Found
GET /users?updated-since=date → 304 Not Modified (if unchanged)
GET /users (auth required) → 401 Unauthorized or 403 Forbidden
Best practice: Always use 200 OK for successful GET.
POST - Creating Resources
POST /users → 201 Created + Location header
POST /users (invalid data) → 400 Bad Request or 422 Unprocessable
POST /users (duplicate) → 409 Conflict
POST /async-job → 202 Accepted + Location for tracking
POST /protected → 401 Unauthorized or 403 Forbidden
Best practice: Return 201 with Location header pointing to created resource.
PUT - Replacing Resources
PUT /users/123 → 200 OK (return updated resource)
PUT /users/123 → 204 No Content (if not returning body)
PUT /users/123 (not found) → 404 Not Found or 201 Created (for create)
PUT /users/123 (conflict) → 409 Conflict (if versioned)
PUT /protected → 401 or 403
Best practice: Return 200 OK with updated resource or 204 No Content.
PATCH - Partial Updates
PATCH /users/123 → 200 OK (return updated resource)
PATCH /users/123 → 204 No Content
PATCH /users/123 (conflict) → 409 Conflict
PATCH /users/123 (invalid) → 400 Bad Request or 422 Unprocessable
Best practice: Similar to PUT; return updated resource or 204.
DELETE - Deleting Resources
DELETE /users/123 → 204 No Content
DELETE /users/123 → 200 OK (with deletion confirmation)
DELETE /users/999 → 404 Not Found
DELETE /users/123 (conflict) → 409 Conflict (has dependencies)
DELETE /protected → 401 or 403
Best practice: Return 204 No Content (safest) or 200 OK with confirmation.
Status Code Decision Tree
START: Client makes request
Does request succeed?
├─ YES
│ └─ Is this a resource creation (POST)?
│ ├─ YES → 201 Created (with Location header)
│ ├─ NO → Is there response content?
│ ├─ YES → 200 OK
│ ├─ NO → 204 No Content
│ ├─ Is this async processing?
│ └─ YES → 202 Accepted (with tracking Location)
│
├─ NO
│ ├─ Is the request format bad?
│ │ └─ YES → 400 Bad Request
│ │
│ ├─ Is this missing/invalid authentication?
│ │ └─ YES → 401 Unauthorized
│ │
│ ├─ Is user authenticated but not authorized?
│ │ └─ YES → 403 Forbidden
│ │
│ ├─ Does the resource not exist?
│ │ └─ YES → 404 Not Found
│ │
│ ├─ Does content validation fail?
│ │ └─ YES → 422 Unprocessable Entity
│ │
│ ├─ Does the request conflict with current state?
│ │ └─ YES → 409 Conflict
│ │
│ ├─ Did the server have an unexpected error?
│ │ └─ YES → 500 Internal Server Error
│ │
│ ├─ Is the server temporarily unavailable?
│ │ └─ YES → 503 Service Unavailable
Error Response Format Best Practices
Consistent Error Structure
{
"error": "human-readable message",
"code": "MACHINE_READABLE_CODE",
"status": 400,
"timestamp": "2025-01-31T10:00:00Z",
"request_id": "req-abc-123",
"details": {
"field": "email",
"reason": "Email already registered"
}
}
Validation Error Response
{
"error": "Validation failed",
"code": "VALIDATION_ERROR",
"status": 422,
"errors": [
{
"field": "email",
"message": "Invalid email format"
},
{
"field": "age",
"message": "Must be 18 or older"
}
]
}
Consistency Across Endpoints
Bad - Inconsistent error formats:
GET /users → {"error": "Not found"}
POST /users → {"message": "User not found"}
DELETE /users → {"error_message": "User not found"}
Good - Consistent error format:
GET /users → {"error": "User not found", "code": "NOT_FOUND"}
POST /users → {"error": "User not found", "code": "NOT_FOUND"}
DELETE /users → {"error": "User not found", "code": "NOT_FOUND"}
Handling Common Scenarios
Bulk Operations
For bulk operations, multiple status codes may apply:
POST /users/bulk
[create 10 users, 7 succeed, 3 fail with duplicates]
Option 1 - Partial Success:
202 Accepted
{
"created": 7,
"failed": 3,
"errors": [
{"index": 2, "error": "Duplicate email"}
]
}
Option 2 - Fail Completely:
422 Unprocessable Entity
{
"error": "Bulk operation partially failed",
"details": [...]
}
Recommendation: Document behavior clearly. Partial success (202) more user-friendly.
Long-Running Operations
POST /export/large-dataset
202 Accepted
Location: /export/jobs/abc-123
{
"status": "processing",
"job_id": "abc-123",
"progress": 0
}
[Client polls Job endpoint]
GET /export/jobs/abc-123
200 OK
{
"status": "processing",
"progress": 45
}
[When complete]
GET /export/jobs/abc-123
200 OK
{
"status": "completed",
"progress": 100,
"download_url": "/exports/dataset-abc-123.csv"
}
Conditional Requests
GET /data HTTP/1.1
If-None-Match: "abc123"
Server: Is data unchanged?
├─ YES → 304 Not Modified [no body]
└─ NO → 200 OK [with current data]
Rate Limiting
GET /api/search
429 Too Many Requests
Retry-After: 60
X-RateLimit-Limit: 100
X-RateLimit-Remaining: 0
X-RateLimit-Reset: 1706779200
{"error": "Rate limit exceeded"}
API Versioning with Status Codes
Deprecating API Versions
GET /api/v1/users (old)
301 Moved Permanently
Location: /api/v2/users
[Tells clients to migrate]
vs.
GET /api/v1/users (deprecated)
200 OK
Deprecation: true
Sunset: Sun, 31 Dec 2025 23:59:59 GMT
[Warns about upcoming removal]
Use 301: When old version should never be used Use 200 with warnings: When deprecation window needed
Testing Status Code Implementation
Test Matrix
Operation | Normal | Auth Fail | Conflict | Not Found
GET | 200 | 401/403 | N/A | 404
POST (create)| 201 | 401/403 | 409 | N/A
PUT | 200 | 401/403 | 409 | 404
PATCH | 200 | 401/403 | 409 | 404
DELETE | 204 | 401/403 | 409 | 404
Common Test Cases
def test_api_status_codes():
# GET existing resource
assert get("/users/123").status == 200
# GET non-existing resource
assert get("/users/999").status == 404
# POST create resource
response = post("/users", {"name": "John"})
assert response.status == 201
assert "Location" in response.headers
# POST with auth failure
assert post("/users", {}, headers={}).status == 401
# DELETE
assert delete("/users/123").status == 204
# Conditional request
response1 = get("/data")
etag = response1.headers["ETag"]
response2 = get("/data", headers={"If-None-Match": etag})
assert response2.status == 304
Conclusion
Proper HTTP status code usage in RESTful APIs is fundamental to creating predictable, usable services. By following these practices:
- Use appropriate 2xx codes for success (200, 201, 202, 204)
- Use 3xx for redirects and conditional responses
- Use 4xx for client errors (400, 401, 403, 404, 409, 422, 429)
- Use 5xx for server errors
- Provide consistent error response formats
- Include helpful response headers
- Document status code meanings
APIs that use status codes properly are more usable, more testable, and can be automated more effectively. Invest time getting this right from the beginning of API design.


