Return semantically correct HTTP status codes (400 for bad input, 401 for unauthenticated, 403 for unauthorized, 404 for missing, 500 for server errors). Using 200 for everything hides errors from monitoring, breaks caching, and makes debugging impossible.
Why This Matters
HTTP status codes are a fundamental contract between server and client. When you return 200 for errors, monitoring tools don't alert, CDN caches store error responses, retry logic doesn't trigger, and developers waste hours debugging "successful" requests that contain error payloads. Proper status codes enable automated error detection, correct caching behavior, and meaningful API observability.
HTTP status codes are not just numbers — they drive behavior across the entire infrastructure stack:
Monitoring/alerting: tools like Datadog and Sentry track 4xx/5xx rates. If errors return 200, they're invisible to monitoring.
CDN/caching: CDNs cache 200 responses. A 200 error response gets cached and served to every subsequent user.
Retry logic: clients retry 503 (Service Unavailable) but not 200. Wrong status codes break automatic recovery.
API consumers: developers expect to check response.ok or the status code. A 200 with an error body forces them to parse the response to detect failures.
Using 200 for everything is the API equivalent of returning { success: true, error: "Something went wrong" } — technically valid, practically useless.
The rule
Return the most specific, semantically correct HTTP status code for every response:
Code
Meaning
When to use
200
OK
Successful GET, PUT, PATCH
201
Created
Successful POST that creates a resource
204
No Content
Successful DELETE
400
Bad Request
Invalid input, malformed JSON
401
Unauthorized
Missing or invalid authentication
403
Forbidden
Authenticated but not authorized
404
Not Found
Resource doesn't exist
409
Conflict
Duplicate resource, version conflict
422
Unprocessable Entity
Valid JSON but invalid data
429
Too Many Requests
Rate limit exceeded
500
Internal Server Error
Unhandled server error
Bad example
// BAD: 200 for everythingexport async function POST(request: Request) { try { const body = await request.json(); const user = await db.user.findUnique({ where: { id: body.id } }); if (!user) { return Response.json({ error: "User not found" }); // 200! } return Response.json({ data: user }); // 200 } catch (error) { return Response.json({ error: "Something went wrong" }); // 200! }}