API Response Codes Best Practices: HTTP Status Codes for REST APIs
Why HTTP Status Codes Matter for APIs
HTTP status codes are the backbone of client-server communication in REST APIs. They tell clients whether a request succeeded, failed due to client error, or failed due to server error -- before the client even reads the response body. Proper use of status codes enables automatic retry logic, correct caching behavior, accurate monitoring dashboards, and consistent error handling across all API consumers.
Despite their importance, status codes are one of the most commonly misused aspects of API design. Many APIs return 200 OK for everything, including errors. Others use 400 Bad Request as a catch-all for any client mistake. This guide covers the correct status codes for every common API scenario, with real-world examples and error response formats.
For a quick reference of all HTTP status codes, check our HTTP Status Codes lookup tool, or read the comprehensive HTTP Status Codes Guide for detailed explanations of each code.
Success Codes (2xx): When Things Go Right
The 2xx family indicates that the request was received, understood, and processed successfully. Most APIs need only four success codes.
| Code | Name | When to Use |
|---|---|---|
| 200 | OK | GET, PUT, PATCH that returns data |
| 201 | Created | POST that creates a new resource |
| 204 | No Content | DELETE, PUT/PATCH with no response body |
| 202 | Accepted | Async operations (queued for processing) |
// 200 OK - Successful GET
GET /api/users/123
Response: 200 OK
{
"id": 123,
"name": "Jane Doe",
"email": "[email protected]"
}
// 201 Created - Resource created via POST
POST /api/users
Request: { "name": "Jane Doe", "email": "[email protected]" }
Response: 201 Created
Location: /api/users/123
{
"id": 123,
"name": "Jane Doe",
"email": "[email protected]",
"createdAt": "2026-03-07T10:00:00Z"
}
// 204 No Content - Successful DELETE
DELETE /api/users/123
Response: 204 No Content
(empty body)
// 202 Accepted - Async operation
POST /api/reports/generate
Response: 202 Accepted
{
"jobId": "rpt-456",
"status": "processing",
"statusUrl": "/api/jobs/rpt-456"
}A common mistake is using 200 for POST requests that create resources. Use 201 Created instead, and include a Location header pointing to the newly created resource. For DELETE operations, 204 No Content is the correct response because there is nothing to return.
Client Error Codes (4xx): The Client Did Something Wrong
The 4xx family indicates that the error was caused by the client. The request contained invalid data, was missing authentication, or targeted a resource that does not exist. Choosing the right 4xx code tells the client exactly what went wrong and whether retrying with the same request would ever succeed.
| Code | Name | When to Use |
|---|---|---|
| 400 | Bad Request | Malformed syntax, invalid JSON, missing required fields |
| 401 | Unauthorized | Missing or invalid authentication credentials |
| 403 | Forbidden | Authenticated but insufficient permissions |
| 404 | Not Found | Resource does not exist |
| 409 | Conflict | Duplicate resource, version conflict |
| 422 | Unprocessable Entity | Valid syntax but semantic validation failure |
| 429 | Too Many Requests | Rate limit exceeded |
400 vs 422: When to Use Each
// 400 Bad Request - Syntactically malformed request
POST /api/users
Content-Type: application/json
Body: { invalid json here
Response: 400 Bad Request
{
"error": {
"code": "INVALID_JSON",
"message": "Request body is not valid JSON",
"details": "Unexpected token 'i' at position 2"
}
}
// 422 Unprocessable Entity - Valid syntax, invalid data
POST /api/users
Content-Type: application/json
Body: { "email": "not-an-email", "age": -5 }
Response: 422 Unprocessable Entity
{
"error": {
"code": "VALIDATION_ERROR",
"message": "Request body failed validation",
"fields": [
{ "field": "email", "message": "Must be a valid email address" },
{ "field": "age", "message": "Must be a positive integer" }
]
}
}401 vs 403: Authentication vs Authorization
This is one of the most commonly confused pairs. 401 Unauthorized means "I do not know who you are" (missing or invalid credentials). 403 Forbidden means "I know who you are, but you are not allowed to do this" (insufficient permissions). A 401 response should include a WWW-Authenticate header. A 403 should not, because re-authenticating will not help.
For API authentication best practices, including JWT tokens vs session cookies, see our JWT vs Session Cookies comparison guide. You can inspect JWT tokens with our JWT Decoder tool.
Server Error Codes (5xx): Something Broke on Your End
The 5xx family indicates that the server failed to fulfill a valid request. The client did nothing wrong, and retrying the same request later might succeed. Unlike 4xx errors, 5xx errors should trigger alerts in your monitoring system.
| Code | Name | When to Use |
|---|---|---|
| 500 | Internal Server Error | Unhandled exception, unexpected failure |
| 502 | Bad Gateway | Upstream service returned invalid response |
| 503 | Service Unavailable | Server overloaded, maintenance mode |
| 504 | Gateway Timeout | Upstream service did not respond in time |
// 500 Internal Server Error
Response: 500 Internal Server Error
{
"error": {
"code": "INTERNAL_ERROR",
"message": "An unexpected error occurred",
"requestId": "req-abc-123"
}
}
// NEVER expose stack traces, SQL queries, or internal details in production
// 503 Service Unavailable (planned maintenance)
Response: 503 Service Unavailable
Retry-After: 3600
{
"error": {
"code": "MAINTENANCE",
"message": "Service is undergoing scheduled maintenance",
"estimatedResolution": "2026-03-07T12:00:00Z"
}
}For 5xx errors, never expose internal details like stack traces, database queries, or file paths. Instead, log the full error server-side with a requestId and return only the ID to the client. This enables debugging without creating security vulnerabilities. Include a Retry-After header with 503 responses so clients know when to try again.
Designing a Consistent Error Response Format
A consistent error format across all endpoints is critical for API usability. Every error response should include the same top-level structure so client code can handle errors uniformly without checking which endpoint returned the error.
// RFC 7807: Problem Details for HTTP APIs (recommended standard)
{
"type": "https://api.example.com/errors/validation-error",
"title": "Validation Error",
"status": 422,
"detail": "The request body contains invalid field values",
"instance": "/api/users",
"errors": [
{
"field": "email",
"code": "INVALID_FORMAT",
"message": "Must be a valid email address"
},
{
"field": "age",
"code": "OUT_OF_RANGE",
"message": "Must be between 1 and 150"
}
],
"requestId": "req-abc-123",
"timestamp": "2026-03-07T10:30:00Z"
}
// Content-Type for RFC 7807
Content-Type: application/problem+jsonThe RFC 7807 format is becoming the industry standard for API error responses. It provides a structured way to communicate errors with machine-readable type URIs and human-readable detail messages. Even if you do not adopt RFC 7807 fully, your error format should include at minimum: an error code, a human-readable message, and a request ID for debugging. Validate your error response JSON with our JSON Formatter.
Pagination Response Patterns
List endpoints must implement pagination to avoid returning unbounded result sets. There are three common pagination strategies, each with different trade-offs for performance, usability, and real-time data consistency.
// Offset-based pagination (simple, common)
GET /api/users?page=2&limit=25
Response: 200 OK
{
"data": [...],
"pagination": {
"page": 2,
"limit": 25,
"totalItems": 150,
"totalPages": 6,
"hasMore": true
}
}
// Cursor-based pagination (better for large datasets)
GET /api/users?cursor=eyJpZCI6MTAwfQ&limit=25
Response: 200 OK
{
"data": [...],
"pagination": {
"nextCursor": "eyJpZCI6MTI1fQ",
"prevCursor": "eyJpZCI6NzV9",
"hasMore": true,
"limit": 25
}
}
// Link header pagination (GitHub style)
Link: <https://api.example.com/users?page=3&limit=25>; rel="next",
<https://api.example.com/users?page=6&limit=25>; rel="last",
<https://api.example.com/users?page=1&limit=25>; rel="first"Cursor-based pagination is preferred for large, frequently-updated datasets because it avoids the "shifting window" problem where items are skipped or duplicated when data changes between page requests. The cursor is typically a Base64-encoded value containing the sort key of the last item on the current page.
Rate Limiting Headers
Every public API should implement rate limiting and communicate limits clearly through response headers. This prevents abuse, ensures fair usage, and lets clients self-regulate their request rate.
// Rate limit headers (include in EVERY response)
X-RateLimit-Limit: 1000 // Max requests per window
X-RateLimit-Remaining: 847 // Requests remaining
X-RateLimit-Reset: 1709812800 // Unix timestamp when window resets
// IETF draft standard (newer, preferred)
RateLimit-Limit: 1000
RateLimit-Remaining: 847
RateLimit-Reset: 153 // Seconds until reset (not timestamp)
// When rate limit is exceeded
Response: 429 Too Many Requests
Retry-After: 153
{
"error": {
"code": "RATE_LIMIT_EXCEEDED",
"message": "Too many requests. Please retry after 153 seconds.",
"limit": 1000,
"window": "1h",
"retryAfter": 153
}
}Include rate limit headers in every response, not just 429 responses. This allows clients to track their usage proactively. The Retry-After header should contain seconds (not a date) for easier client-side processing. Consider implementing different rate limits for different endpoints, authentication levels, and pricing tiers.
Status Codes for CRUD Operations
Here is a quick-reference mapping of HTTP methods to status codes for standard CRUD operations. This covers the most common API patterns.
| Operation | Method | Success | Common Errors |
|---|---|---|---|
| List all | GET /items | 200 | 401, 403 |
| Get one | GET /items/:id | 200 | 401, 403, 404 |
| Create | POST /items | 201 | 400, 401, 409, 422 |
| Full update | PUT /items/:id | 200 | 400, 401, 403, 404, 422 |
| Partial update | PATCH /items/:id | 200 | 400, 401, 403, 404, 422 |
| Delete | DELETE /items/:id | 204 | 401, 403, 404 |
For a complete reference of all HTTP status codes, use our HTTP Status Codes tool to look up any code instantly.
Implementing Error Handling in Express.js
// Custom API error class
class ApiError extends Error {
constructor(statusCode, code, message, details = null) {
super(message);
this.statusCode = statusCode;
this.code = code;
this.details = details;
}
}
// Validation middleware
function validateUser(req, res, next) {
const errors = [];
if (!req.body.email) {
errors.push({ field: 'email', message: 'Email is required' });
}
if (req.body.email && !isValidEmail(req.body.email)) {
errors.push({ field: 'email', message: 'Must be a valid email' });
}
if (errors.length > 0) {
throw new ApiError(422, 'VALIDATION_ERROR', 'Validation failed', errors);
}
next();
}
// Global error handler (must be last middleware)
app.use((err, req, res, next) => {
const statusCode = err.statusCode || 500;
const response = {
error: {
code: err.code || 'INTERNAL_ERROR',
message: statusCode === 500
? 'An unexpected error occurred'
: err.message,
requestId: req.id,
},
};
if (err.details) response.error.fields = err.details;
if (statusCode === 500) console.error(err.stack);
res.status(statusCode).json(response);
});Use a centralized error handler to ensure all errors are formatted consistently. Never let unhandled exceptions leak internal details like stack traces or database errors to the client. Validate request parameters early using regex patterns -- our Regex Tester and validation patterns guide can help you build robust input validation.
API Response Best Practices Checklist
- Use the correct status code for every response -- Do not return
200for errors. Use the specific 4xx/5xx code that matches the situation. - Return consistent error formats -- Use RFC 7807 or a similar structured format across all endpoints. Include error code, message, and request ID.
- Include rate limit headers in every response -- Not just in
429responses. Let clients track their usage proactively. - Never expose internal details in production errors -- Stack traces, SQL queries, and file paths are security vulnerabilities. Log them server-side with a request ID instead.
- Use 201 for resource creation -- Include a
Locationheader pointing to the new resource URI. - Use 204 for operations with no response body -- DELETE and PUT/PATCH that do not return updated data.
- Implement idempotency for POST/PATCH -- Accept an
Idempotency-Keyheader to prevent duplicate operations from network retries. - Document every possible response code per endpoint -- Use OpenAPI/Swagger to document all success and error responses with example bodies.
- Version your API -- Use URL versioning (
/v1/) or header versioning so you can evolve error formats without breaking existing clients. - Test error paths as thoroughly as success paths -- Most API bugs live in error handling code. Write tests for every 4xx and 5xx scenario.
Frequently Asked Questions
Should I return 200 with an error object or a proper 4xx/5xx status code?
Always use proper HTTP status codes. Returning 200 for errors (sometimes called "200 OK with error body") breaks HTTP semantics, confuses monitoring tools, prevents proper caching, and makes client-side error handling more complex. Use 4xx for client errors and 5xx for server errors, with a structured error body that provides additional context like error codes, messages, and documentation links.
When should I use 400 Bad Request vs 422 Unprocessable Entity?
Use 400 when the request is syntactically malformed -- invalid JSON, missing required headers, or wrong Content-Type. Use 422 when the request is syntactically correct but semantically invalid -- an email field that is not a valid email address, or a date range where the start is after the end. In practice, many APIs use 400 for both cases, which is acceptable.
How should I handle rate limiting in my API?
Return 429 Too Many Requests when a client exceeds the rate limit. Include a Retry-After header indicating how many seconds to wait. Also include rate limit headers (X-RateLimit-Limit, X-RateLimit-Remaining, X-RateLimit-Reset) in every response so clients can proactively manage their request rate.
Look Up Any HTTP Status Code
Quickly find the meaning, use case, and best practices for any HTTP status code. Our free reference tool covers every code from 100 to 599 with developer-friendly explanations.
Open HTTP Status Codes ReferenceRelated Articles
HTTP Status Codes Guide
Every HTTP status code explained with real-world context.
JWT vs Session Cookies
Authentication methods compared for APIs and web apps.
JSON Formatting Guide
Format, validate, and minify JSON API responses.
Regex Validation Patterns
Production-ready patterns for email, URL, phone validation.