BytePane

API Response Codes Best Practices: HTTP Status Codes for REST APIs

Web Development14 min read

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.

CodeNameWhen to Use
200OKGET, PUT, PATCH that returns data
201CreatedPOST that creates a new resource
204No ContentDELETE, PUT/PATCH with no response body
202AcceptedAsync 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.

CodeNameWhen to Use
400Bad RequestMalformed syntax, invalid JSON, missing required fields
401UnauthorizedMissing or invalid authentication credentials
403ForbiddenAuthenticated but insufficient permissions
404Not FoundResource does not exist
409ConflictDuplicate resource, version conflict
422Unprocessable EntityValid syntax but semantic validation failure
429Too Many RequestsRate 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.

CodeNameWhen to Use
500Internal Server ErrorUnhandled exception, unexpected failure
502Bad GatewayUpstream service returned invalid response
503Service UnavailableServer overloaded, maintenance mode
504Gateway TimeoutUpstream 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+json

The 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.

OperationMethodSuccessCommon Errors
List allGET /items200401, 403
Get oneGET /items/:id200401, 403, 404
CreatePOST /items201400, 401, 409, 422
Full updatePUT /items/:id200400, 401, 403, 404, 422
Partial updatePATCH /items/:id200400, 401, 403, 404, 422
DeleteDELETE /items/:id204401, 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

  1. Use the correct status code for every response -- Do not return 200 for errors. Use the specific 4xx/5xx code that matches the situation.
  2. Return consistent error formats -- Use RFC 7807 or a similar structured format across all endpoints. Include error code, message, and request ID.
  3. Include rate limit headers in every response -- Not just in 429 responses. Let clients track their usage proactively.
  4. 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.
  5. Use 201 for resource creation -- Include a Location header pointing to the new resource URI.
  6. Use 204 for operations with no response body -- DELETE and PUT/PATCH that do not return updated data.
  7. Implement idempotency for POST/PATCH -- Accept an Idempotency-Key header to prevent duplicate operations from network retries.
  8. Document every possible response code per endpoint -- Use OpenAPI/Swagger to document all success and error responses with example bodies.
  9. Version your API -- Use URL versioning (/v1/) or header versioning so you can evolve error formats without breaking existing clients.
  10. 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 Reference

Related Articles