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.
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.
BeforeMerge scans your pull requests against this rule and 4+ others. Get actionable feedback before code ships.
HTTP status codes are not just numbers — they drive behavior across the entire infrastructure stack:
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.
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: 200 for everything
export 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!
}
}// GOOD: semantically correct status codes
export async function POST(request: Request) {
try {
const body = await request.json();
if (!body.id) {
return Response.json(
{ error: "Missing required field: id" },
{ status: 400 }
);
}
const user = await db.user.findUnique({ where: { id: body.id } });
if (!user) {
return Response.json(
{ error: "User not found" },
{ status: 404 }
);
}
return Response.json({ data: user }, { status: 200 });
} catch (error) {
console.error("Unexpected error:", error);
return Response.json(
{ error: "Internal server error" },
{ status: 500 }
);
}
}Search for Response.json calls without explicit status codes:
grep -n "Response.json(" --include="*.ts" --include="*.tsx" -r app/api/Any Response.json({ error: ... }) without a status parameter defaults to 200.
{ status: 4xx } for client errors, { status: 5xx } for server errorserrorResponse(status, message) and successResponse(data, status)