Home/Blog/How should APIs use status codes for RESTful responses?
Development

How should APIs use status codes for RESTful responses?

Learn RESTful API best practices for using HTTP status codes to provide clear semantics and predictable behavior.

By Inventive HQ Team
How should APIs use status codes for RESTful responses?

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:

  1. Use appropriate 2xx codes for success (200, 201, 202, 204)
  2. Use 3xx for redirects and conditional responses
  3. Use 4xx for client errors (400, 401, 403, 404, 409, 422, 429)
  4. Use 5xx for server errors
  5. Provide consistent error response formats
  6. Include helpful response headers
  7. 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.

Need Expert IT & Security Guidance?

Want to explore HTTP status codes further? Use our HTTP Status Code Lookup tool for instant explanations and our HTTP Request Builder to test your API responses effortlessly.