Your API returns 200 OK for everything. Successful creation? 200 OK. Validation error? 200 OK with an error message in the body. Server crash? 200 OK with {"success": false}.
This is wrong. HTTP status codes exist for a reason. They tell clients what happened without parsing the response body. They enable proper error handling, caching, and retry logic.
Here's how to use HTTP status codes correctly in REST APIs.
The Five Status Code Categories
HTTP status codes are grouped into five categories:
- 1xx (Informational): Request received, continuing process
- 2xx (Success): Request successfully received, understood, and accepted
- 3xx (Redirection): Further action needed to complete the request
- 4xx (Client Error): Request contains bad syntax or can't be fulfilled
- 5xx (Server Error): Server failed to fulfill a valid request
Most REST APIs use 2xx, 4xx, and 5xx codes. Let's break down when to use each one.
Success Codes (2xx)
200 OK
Use 200 for successful GET, PUT, or PATCH requests that return a response body.
GET request:
GET /pets/019b4132-70aa-764f-b315-e2803d882a24
Response: 200 OK
{
"id": "019b4132-70aa-764f-b315-e2803d882a24",
"name": "Max",
"species": "DOG",
"status": "AVAILABLE"
}
PUT request (full replacement):
PUT /pets/019b4132-70aa-764f-b315-e2803d882a24
{
"name": "Max",
"species": "DOG",
"status": "ADOPTED"
}
Response: 200 OK
{
"id": "019b4132-70aa-764f-b315-e2803d882a24",
"name": "Max",
"species": "DOG",
"status": "ADOPTED",
"updatedAt": "2026-03-13T10:30:00Z"
}
PATCH request (partial update):
PATCH /pets/019b4132-70aa-764f-b315-e2803d882a24
{
"status": "ADOPTED"
}
Response: 200 OK
{
"id": "019b4132-70aa-764f-b315-e2803d882a24",
"name": "Max",
"species": "DOG",
"status": "ADOPTED",
"updatedAt": "2026-03-13T10:30:00Z"
}
Don't use 200 for POST (creation) or DELETE operations. Those have better status codes.
201 Created
Use 201 when a POST request successfully creates a resource.
POST /pets
{
"name": "Bella",
"species": "CAT",
"breed": "Siamese"
}
Response: 201 Created
Location: /pets/019b4145-8f3a-7c2d-a1b5-f4e8d9c3a7b2
{
"id": "019b4145-8f3a-7c2d-a1b5-f4e8d9c3a7b2",
"name": "Bella",
"species": "CAT",
"breed": "Siamese",
"status": "AVAILABLE",
"createdAt": "2026-03-13T10:35:00Z"
}
The Location header tells clients where to find the new resource. This is important for clients that need to perform follow-up operations.
202 Accepted
Use 202 when the request is valid but processing happens asynchronously.
POST /pets/019b4132-70aa-764f-b315-e2803d882a24/medical-records
{
"type": "VACCINATION",
"vaccine": "Rabies",
"date": "2026-03-13"
}
Response: 202 Accepted
{
"id": "019b4150-2a1f-6d4e-b8c3-e7f9a2d5c1b8",
"status": "PROCESSING",
"statusUrl": "/medical-records/019b4150-2a1f-6d4e-b8c3-e7f9a2d5c1b8/status"
}
The response includes a URL where clients can check processing status. This pattern works well for long-running operations like file processing, report generation, or external API calls.
204 No Content
Use 204 for successful DELETE requests or updates that don't return a body.
DELETE request:
DELETE /pets/019b4132-70aa-764f-b315-e2803d882a24
Response: 204 No Content
No response body. The status code alone confirms success.
PUT request without response:
PUT /users/019b4155-7e2c-5a3f-d9b4-c8e1f3a6b2d7/preferences
{
"emailNotifications": true,
"theme": "dark"
}
Response: 204 No Content
Use 204 when the client doesn't need the updated resource. This saves bandwidth.
Client Error Codes (4xx)
400 Bad Request
Use 400 for malformed requests—invalid JSON, missing required headers, or syntax errors.
POST /pets
{
"name": "Max",
"species": "INVALID_SPECIES" ← Not a valid enum value
}
Response: 400 Bad Request
{
"type": "https://petstoreapi.com/errors/bad-request",
"title": "Bad Request",
"status": 400,
"detail": "Request body contains invalid data",
"instance": "/v1/pets"
}
Don't use 400 for validation errors. Use 422 instead (see below).
401 Unauthorized
Use 401 when authentication is required but missing or invalid.
GET /pets/019b4132-70aa-764f-b315-e2803d882a24
Response: 401 Unauthorized
WWW-Authenticate: Bearer realm="PetStore API"
{
"type": "https://petstoreapi.com/errors/unauthorized",
"title": "Unauthorized",
"status": 401,
"detail": "Authentication required. Provide a valid access token."
}
The WWW-Authenticate header tells clients how to authenticate.
403 Forbidden
Use 403 when the user is authenticated but lacks permission.
DELETE /pets/019b4132-70aa-764f-b315-e2803d882a24
Response: 403 Forbidden
{
"type": "https://petstoreapi.com/errors/forbidden",
"title": "Forbidden",
"status": 403,
"detail": "You don't have permission to delete this pet",
"requiredScope": "write:pets"
}
The difference between 401 and 403: - 401: "Who are you?" (authentication problem) - 403: "I know who you are, but you can't do that" (authorization problem)
404 Not Found
Use 404 when the requested resource doesn't exist.
GET /pets/019b4999-9999-9999-9999-999999999999
Response: 404 Not Found
{
"type": "https://petstoreapi.com/errors/not-found",
"title": "Not Found",
"status": 404,
"detail": "Pet not found",
"instance": "/v1/pets/019b4999-9999-9999-9999-999999999999"
}
Also use 404 for invalid endpoints:
GET /invalid-endpoint
Response: 404 Not Found
405 Method Not Allowed
Use 405 when the HTTP method isn't supported for the endpoint.
POST /pets/019b4132-70aa-764f-b315-e2803d882a24
Response: 405 Method Not Allowed
Allow: GET, PUT, PATCH, DELETE
{
"type": "https://petstoreapi.com/errors/method-not-allowed",
"title": "Method Not Allowed",
"status": 405,
"detail": "POST is not allowed for this endpoint",
"allowedMethods": ["GET", "PUT", "PATCH", "DELETE"]
}
The Allow header lists supported methods.
409 Conflict
Use 409 when the request conflicts with the current state.
POST /pets
{
"id": "019b4132-70aa-764f-b315-e2803d882a24", ← ID already exists
"name": "Max"
}
Response: 409 Conflict
{
"type": "https://petstoreapi.com/errors/conflict",
"title": "Conflict",
"status": 409,
"detail": "A pet with this ID already exists"
}
Also use 409 for concurrent modification conflicts:
PUT /pets/019b4132-70aa-764f-b315-e2803d882a24
If-Match: "v1"
Response: 409 Conflict
{
"type": "https://petstoreapi.com/errors/conflict",
"title": "Conflict",
"status": 409,
"detail": "Resource was modified by another request",
"currentVersion": "v2"
}
422 Unprocessable Entity
Use 422 for validation errors—the request is well-formed but contains invalid data.
POST /pets
{
"name": "", ← Empty name
"species": "DOG",
"age": -5 ← Negative age
}
Response: 422 Unprocessable Entity
{
"type": "https://petstoreapi.com/errors/validation-error",
"title": "Validation Error",
"status": 422,
"detail": "The request body contains validation errors",
"errors": [
{
"field": "name",
"message": "Name cannot be empty"
},
{
"field": "age",
"message": "Age must be a positive number"
}
]
}
The errors array lists all validation failures. This helps clients fix multiple issues at once.
429 Too Many Requests
Use 429 when the client exceeds rate limits.
GET /pets
Response: 429 Too Many Requests
X-RateLimit-Limit: 1000
X-RateLimit-Remaining: 0
X-RateLimit-Reset: 1678886400
Retry-After: 3600
{
"type": "https://petstoreapi.com/errors/rate-limit-exceeded",
"title": "Rate Limit Exceeded",
"status": 429,
"detail": "You have exceeded the rate limit of 1000 requests per hour",
"retryAfter": 3600
}
The Retry-After header tells clients when they can retry.
Server Error Codes (5xx)
500 Internal Server Error
Use 500 for unexpected server errors.
GET /pets/019b4132-70aa-764f-b315-e2803d882a24
Response: 500 Internal Server Error
{
"type": "https://petstoreapi.com/errors/internal-error",
"title": "Internal Server Error",
"status": 500,
"detail": "An unexpected error occurred",
"instance": "/v1/pets/019b4132-70aa-764f-b315-e2803d882a24",
"traceId": "abc123def456"
}
Include a traceId for debugging. Don't expose stack traces or internal details in production.
502 Bad Gateway
Use 502 when an upstream service fails.
GET /pets/019b4132-70aa-764f-b315-e2803d882a24/weather
Response: 502 Bad Gateway
{
"type": "https://petstoreapi.com/errors/bad-gateway",
"title": "Bad Gateway",
"status": 502,
"detail": "Weather service is unavailable"
}
This tells clients the problem is with a dependency, not your API.
503 Service Unavailable
Use 503 during maintenance or when the service is temporarily down.
GET /pets
Response: 503 Service Unavailable
Retry-After: 300
{
"type": "https://petstoreapi.com/errors/service-unavailable",
"title": "Service Unavailable",
"status": 503,
"detail": "The service is temporarily unavailable for maintenance",
"retryAfter": 300
}
The Retry-After header tells clients when to try again.
504 Gateway Timeout
Use 504 when an upstream service times out.
GET /pets/019b4132-70aa-764f-b315-e2803d882a24/recommendations
Response: 504 Gateway Timeout
{
"type": "https://petstoreapi.com/errors/gateway-timeout",
"title": "Gateway Timeout",
"status": 504,
"detail": "Recommendation service did not respond in time"
}
Common Mistakes
Mistake 1: Using 200 for Everything
Don't return 200 with {"success": false} in the body. Use proper error codes.
Bad:
POST /pets
{
"name": ""
}
Response: 200 OK
{
"success": false,
"error": "Name is required"
}
Good:
Response: 422 Unprocessable Entity
{
"type": "https://petstoreapi.com/errors/validation-error",
"title": "Validation Error",
"status": 422,
"detail": "Name is required"
}
Mistake 2: Using 200 for Creation
Don't return 200 for POST requests that create resources. Use 201.
Bad:
POST /pets
Response: 200 OK
Good:
Response: 201 Created
Location: /pets/019b4132-70aa-764f-b315-e2803d882a24
Mistake 3: Using 200 for Deletion
Don't return 200 with a body for DELETE requests. Use 204.
Bad:
DELETE /pets/019b4132-70aa-764f-b315-e2803d882a24
Response: 200 OK
{
"message": "Pet deleted successfully"
}
Good:
Response: 204 No Content
Mistake 4: Confusing 400 and 422
Use 400 for malformed requests, 422 for validation errors.
400 example (malformed JSON):
POST /pets
{
"name": "Max",
"species": "DOG" ← Missing closing brace
Response: 400 Bad Request
422 example (valid JSON, invalid data):
POST /pets
{
"name": "",
"species": "DOG"
}
Response: 422 Unprocessable Entity
Mistake 5: Confusing 401 and 403
Use 401 for authentication problems, 403 for authorization problems.
401 example (no token):
GET /pets
Response: 401 Unauthorized
403 example (valid token, insufficient permissions):
DELETE /pets/019b4132-70aa-764f-b315-e2803d882a24
Response: 403 Forbidden
Status Code Decision Tree
Use this flowchart to choose the right status code:
- Was the request successful?
- Yes → Go to step 2
No → Go to step 5
Did you create a resource?
- Yes → 201 Created
No → Go to step 3
Are you returning a response body?
- Yes → 200 OK
No → 204 No Content
Is processing asynchronous?
- Yes → 202 Accepted
No → 200 OK
Is it a client error?
- Yes → Go to step 6
No → Go to step 10
Is the request malformed?
- Yes → 400 Bad Request
No → Go to step 7
Is authentication missing or invalid?
- Yes → 401 Unauthorized
No → Go to step 8
Does the user lack permission?
- Yes → 403 Forbidden
No → Go to step 9
Does the resource not exist?
- Yes → 404 Not Found
No → Check for 405, 409, 422, or 429
Is it a server error?
- Internal error → 500 Internal Server Error
- Upstream failure → 502 Bad Gateway
- Service down → 503 Service Unavailable
- Upstream timeout → 504 Gateway Timeout
Best Practices
1. Be Consistent
Use the same status codes for the same situations across your API. Don't return 200 for creation in one endpoint and 201 in another.
2. Include Error Details
Always return a structured error response with: - Error type (URL to documentation) - Human-readable title - Detailed message - Status code - Request instance (the URL that failed)
Follow RFC 9457 Problem Details format.
3. Use Standard Headers
Include relevant headers: - Location for 201 responses - WWW-Authenticate for 401 responses - Allow for 405 responses - Retry-After for 429 and 503 responses
4. Don't Expose Internal Details
In production, don't return stack traces, database errors, or internal paths. Log these server-side and return a generic error to clients.
5. Document Your Status Codes
List all possible status codes for each endpoint in your API documentation. Include example responses for each code.
Testing Status Codes
Test that your API returns correct status codes:
// Test 201 for creation
const response = await fetch('/pets', {
method: 'POST',
body: JSON.stringify({name: 'Max', species: 'DOG'})
});
assert(response.status === 201);
assert(response.headers.get('Location'));
// Test 404 for missing resource
const response = await fetch('/pets/invalid-id');
assert(response.status === 404);
// Test 422 for validation errors
const response = await fetch('/pets', {
method: 'POST',
body: JSON.stringify({name: '', species: 'DOG'})
});
assert(response.status === 422);
Use tools like Apidog to test all status code scenarios and ensure your API behaves correctly.
Conclusion
HTTP status codes are not optional. They're part of the HTTP specification and enable proper client behavior.
Use 2xx for success, 4xx for client errors, 5xx for server errors. Be specific: 201 for creation, 204 for deletion, 422 for validation, 401 for authentication, 403 for authorization.
Your clients will thank you. Their error handling will be simpler, their retry logic will work correctly, and their caching will be more efficient.
Stop returning 200 for everything. Use the right status code for the right situation.