Skip to content

REST API Error Handling

Complete reference for HTTP status codes, error response formats, and troubleshooting common API errors.

All API errors follow a consistent JSON format:

{
"error": "Error type",
"message": "Human-readable description",
"code": "ERROR_CODE",
"details": {
"field": "Additional context"
}
}

Fields:

  • error - Error category (string)
  • message - Human-readable error description
  • code - Machine-readable error code (uppercase snake_case)
  • details - Optional additional context (object)

Request succeeded with data in response.

GET /api/users/user-123
Authorization: Bearer <token>
HTTP/1.1 200 OK
Content-Type: application/json
{
"id": "user-123",
"email": "user@example.com",
"profile": {
"firstName": "John"
}
}

Resource created successfully.

POST /api/users
Authorization: Bearer <token>
Content-Type: application/json
HTTP/1.1 201 Created
Content-Type: application/json
Location: /api/users/user-456
{
"success": true,
"userId": "user-456",
"email": "newuser@example.com"
}

Request succeeded with no response body.

DELETE /api/users/user-123
Authorization: Bearer <token>
HTTP/1.1 204 No Content

Invalid request syntax or validation errors.

POST /api/users
Authorization: Bearer <token>
Content-Type: application/json
{
"email": "invalid-email"
}
HTTP/1.1 400 Bad Request
Content-Type: application/json
{
"error": "Validation failed",
"message": "Request validation failed",
"code": "VALIDATION_ERROR",
"details": {
"email": "Invalid email format",
"password": "Password is required"
}
}

Common causes:

  • Missing required fields
  • Invalid field values
  • Malformed JSON
  • Type mismatches

Missing or invalid authentication.

GET /api/users/user-123
HTTP/1.1 401 Unauthorized
Content-Type: application/json
{
"error": "Unauthorized",
"message": "No authorization header provided",
"code": "NO_AUTH_HEADER"
}

Common causes:

  • Missing Authorization header
  • Invalid JWT token
  • Expired token
  • Token signature verification failed

Variations:

Invalid Token:

{
"error": "Unauthorized",
"message": "Invalid or expired token",
"code": "INVALID_TOKEN"
}

Token Expired:

{
"error": "Unauthorized",
"message": "Token has expired",
"code": "TOKEN_EXPIRED"
}

Authenticated but insufficient permissions.

POST /api/users
Authorization: Bearer <token>
Content-Type: application/json
HTTP/1.1 403 Forbidden
Content-Type: application/json
{
"error": "Forbidden",
"message": "Missing required permission: users:create",
"code": "INSUFFICIENT_PERMISSIONS",
"requiredPermission": "users:create"
}

Common causes:

  • Missing required permissions
  • Policy violation (business rule)
  • Resource access denied

Variations:

Policy Violation:

{
"error": "Forbidden",
"message": "Policy violation: Users can only update their own profile",
"code": "POLICY_VIOLATION"
}

Resource does not exist.

GET /api/users/user-999
Authorization: Bearer <token>
HTTP/1.1 404 Not Found
Content-Type: application/json
{
"error": "Not Found",
"message": "User not found",
"code": "NOT_FOUND",
"resourceType": "User",
"resourceId": "user-999"
}

Common causes:

  • Invalid resource ID
  • Resource deleted
  • Incorrect URL path

Request conflicts with current state.

POST /api/users
Authorization: Bearer <token>
Content-Type: application/json
{
"email": "existing@example.com",
"password": "password123"
}
HTTP/1.1 409 Conflict
Content-Type: application/json
{
"error": "Conflict",
"message": "User with this email already exists",
"code": "RESOURCE_CONFLICT",
"conflictingField": "email"
}

Common causes:

  • Duplicate unique field (email, username)
  • Concurrent modification
  • Business rule violation

Semantically incorrect request.

POST /api/users
Authorization: Bearer <token>
Content-Type: application/json
{
"email": "user@example.com",
"password": "123",
"profile": {
"firstName": ""
}
}
HTTP/1.1 422 Unprocessable Entity
Content-Type: application/json
{
"error": "Validation failed",
"message": "Input validation failed",
"code": "VALIDATION_ERROR",
"validationErrors": [
{
"field": "password",
"message": "Password must be at least 8 characters"
},
{
"field": "profile.firstName",
"message": "First name cannot be empty"
}
]
}

Common causes:

  • Business rule violations
  • Complex validation errors
  • Invalid state transitions

Rate limit exceeded.

GET /api/users
Authorization: Bearer <token>
HTTP/1.1 429 Too Many Requests
Content-Type: application/json
X-RateLimit-Limit: 1000
X-RateLimit-Remaining: 0
X-RateLimit-Reset: 1700060400
{
"error": "Too Many Requests",
"message": "Rate limit exceeded",
"code": "RATE_LIMIT_EXCEEDED",
"retryAfter": 300
}

Headers:

  • X-RateLimit-Limit - Total requests allowed per window
  • X-RateLimit-Remaining - Requests remaining in window
  • X-RateLimit-Reset - Timestamp when window resets

Unexpected server error.

GET /api/users/user-123
Authorization: Bearer <token>
HTTP/1.1 500 Internal Server Error
Content-Type: application/json
{
"error": "Internal Server Error",
"message": "An unexpected error occurred",
"code": "INTERNAL_ERROR",
"correlationId": "abc-123-def-456"
}

Common causes:

  • Unhandled exception
  • Database connection failure
  • Service unavailable
  • Message bus error

Never includes:

  • Stack traces
  • Internal paths
  • Database errors
  • Sensitive information

Upstream service error.

GET /api/users/user-123
Authorization: Bearer <token>
HTTP/1.1 502 Bad Gateway
Content-Type: application/json
{
"error": "Bad Gateway",
"message": "Upstream service unavailable",
"code": "SERVICE_UNAVAILABLE",
"service": "auth-service"
}

Common causes:

  • Service not responding
  • Message bus timeout
  • Service crashed
  • Network issue

Service temporarily unavailable.

GET /api/users
Authorization: Bearer <token>
HTTP/1.1 503 Service Unavailable
Content-Type: application/json
Retry-After: 60
{
"error": "Service Unavailable",
"message": "Service is temporarily unavailable",
"code": "SERVICE_UNAVAILABLE",
"retryAfter": 60
}

Common causes:

  • Maintenance mode
  • Overloaded service
  • Database connection pool exhausted
  • Circuit breaker open

Upstream service timeout.

GET /api/users
Authorization: Bearer <token>
HTTP/1.1 504 Gateway Timeout
Content-Type: application/json
{
"error": "Gateway Timeout",
"message": "Request timed out",
"code": "TIMEOUT",
"timeoutMs": 30000
}

Common causes:

  • Long-running query
  • Service not responding
  • Message bus timeout
  • Network latency
CodeStatusDescription
NO_AUTH_HEADER401Missing Authorization header
INVALID_TOKEN401Token format invalid or verification failed
TOKEN_EXPIRED401JWT token has expired
INVALID_CREDENTIALS401Email/password authentication failed
CodeStatusDescription
INSUFFICIENT_PERMISSIONS403User lacks required permissions
POLICY_VIOLATION403Business rule policy check failed
ACCESS_DENIED403Resource access not allowed
CodeStatusDescription
VALIDATION_ERROR400/422Input validation failed
MISSING_REQUIRED_FIELD400Required field not provided
INVALID_FORMAT400Field format invalid
INVALID_TYPE400Field type mismatch
CodeStatusDescription
NOT_FOUND404Resource does not exist
RESOURCE_CONFLICT409Resource already exists or conflicts
RESOURCE_DELETED410Resource was deleted
CodeStatusDescription
INTERNAL_ERROR500Unexpected server error
SERVICE_UNAVAILABLE503Service temporarily unavailable
TIMEOUT504Request timeout
RATE_LIMIT_EXCEEDED429Too many requests

Check:

  1. Is Authorization header present?
  2. Is token format correct (Bearer <token>)?
  3. Is token expired? (check exp claim)
  4. Is JWT_SECRET correct?

Fix:

Terminal window
# Get new token
curl -X POST http://localhost:3003/api/auth/authenticate \
-H "Content-Type: application/json" \
-d '{"email":"user@example.com","password":"password"}'
# Use in request
curl -X GET http://localhost:3003/api/users/user-123 \
-H "Authorization: Bearer <new-token>"

Check:

  1. Does user have required permissions?
  2. Check JWT token permissions claim
  3. Review contract @Command or @Query decorator
  4. Check for policy violations

Fix:

Terminal window
# Check user permissions
curl -X GET http://localhost:3003/api/users/user-123/permissions \
-H "Authorization: Bearer <token>"
# Grant required permission
curl -X POST http://localhost:3003/api/users/user-123/permissions \
-H "Authorization: Bearer <admin-token>" \
-H "Content-Type: application/json" \
-d '{"permission":"users:create"}'

Check:

  1. Is resource ID correct?
  2. Does resource exist?
  3. Is URL path correct?
  4. Was resource deleted?

Fix:

Terminal window
# List available resources
curl -X GET http://localhost:3003/api/users \
-H "Authorization: Bearer <token>"
# Check specific resource
curl -X GET http://localhost:3003/api/users/user-123 \
-H "Authorization: Bearer <token>"

Check:

  1. Is email/username unique?
  2. Is there a concurrent modification?
  3. Review business rules

Fix:

Terminal window
# Use different email
curl -X POST http://localhost:3003/api/users \
-H "Authorization: Bearer <token>" \
-H "Content-Type: application/json" \
-d '{"email":"unique@example.com","password":"password123"}'

Check:

  1. Review validation errors in response
  2. Check required fields
  3. Verify field formats
  4. Review business rules

Fix:

Terminal window
# Correct validation errors
curl -X POST http://localhost:3003/api/users \
-H "Authorization: Bearer <token>" \
-H "Content-Type: application/json" \
-d '{
"email":"valid@example.com",
"password":"SecurePassword123!",
"profile":{"firstName":"John","lastName":"Doe"}
}'

Check:

  1. Review server logs
  2. Check correlationId for tracing
  3. Verify service health
  4. Check database connectivity

Fix:

Terminal window
# Check service health
curl -X GET http://localhost:3003/health
# Check logs
docker logs api-gateway
docker logs auth-service
# Restart services if needed
docker compose restart

Check:

  1. Is service running?
  2. Check service health
  3. Review resource usage
  4. Check message bus connectivity

Fix:

Terminal window
# Check service status
docker compose ps
# Check service health
curl -X GET http://localhost:3003/health
# Restart services
docker compose restart auth-service
async function handleApiRequest(url: string, options: RequestInit) {
try {
const response = await fetch(url, options);
// Success
if (response.ok) {
return await response.json();
}
// Parse error
const error = await response.json();
// Handle specific errors
switch (response.status) {
case 401:
// Token expired, try refresh
await refreshToken();
return handleApiRequest(url, options); // Retry
case 403:
// Insufficient permissions
showPermissionError(error.message);
break;
case 404:
// Resource not found
showNotFoundError(error.message);
break;
case 422:
// Validation errors
showValidationErrors(error.validationErrors);
break;
case 429:
// Rate limited
const retryAfter = error.retryAfter || 60;
await sleep(retryAfter * 1000);
return handleApiRequest(url, options); // Retry
case 500:
case 502:
case 503:
// Server errors - retry with backoff
await exponentialBackoff();
return handleApiRequest(url, options); // Retry
default:
showGenericError(error.message);
}
throw new Error(error.message);
} catch (err) {
// Network error or JSON parse error
console.error('Request failed:', err);
throw err;
}
}
function logApiError(error: any, context: Record<string, any>) {
console.error('API Error:', {
status: error.status,
code: error.code,
message: error.message,
correlationId: error.correlationId,
...context
});
}
async function retryWithBackoff(
fn: () => Promise<any>,
maxRetries: number = 3
) {
for (let i = 0; i < maxRetries; i++) {
try {
return await fn();
} catch (error: any) {
// Don't retry client errors (4xx)
if (error.status >= 400 && error.status < 500) {
throw error;
}
// Retry server errors (5xx)
if (i === maxRetries - 1) {
throw error;
}
const delay = Math.min(1000 * 2 ** i, 10000);
await sleep(delay);
}
}
}