HTTP Response Status Codes — Practical Guide for Modern Web & API Development
For developers at Sadeem Informatique
Most developers know what the codes mean — very few consistently use them correctly in real applications. This guide shows practical usage patterns you will actually encounter in production React/Next.js apps + Node.js/Express/Laravel/whatever backend.
Photo by ThisIsEngineering on Unsplash
Quick Reference — Most Used Status Codes in 2026
| Code | Meaning | Typical Use Case (Frontend + Backend) | Body? | Idempotent? |
|---|---|---|---|---|
| 200 | OK | GET — resource returned, PUT/PATCH — updated | Yes | Yes |
| 201 | Created | POST — new resource created, Location header often used | Yes | No |
| 204 | No Content | DELETE successful, PUT/PATCH when no body needed | No | Yes |
| 301 | Moved Permanently | Permanent URL redirection (SEO important) | Sometimes | Yes |
| 400 | Bad Request | Validation failed, malformed JSON, missing required field | Yes | — |
| 401 | Unauthorized | Missing/invalid token, session expired | Yes | — |
| 403 | Forbidden | Authenticated but not allowed (role/permission) | Yes | — |
| 404 | Not Found | Resource does not exist | Yes/No | — |
| 409 | Conflict | Business rule violation (e.g. cannot delete paid invoice) | Yes | — |
| 422 | Unprocessable Entity | Semantic validation failed (WebDAV origin, very common now) | Yes | — |
| 429 | Too Many Requests | Rate limiting hit | Yes | — |
| 500 | Internal Server Error | Unexpected crash — never expose stack trace | Yes | — |
| 502 | Bad Gateway | Proxy / load balancer issue | Sometimes | — |
| 503 | Service Unavailable | Maintenance, overload — should include Retry-After | Yes | — |
Most Important Status Codes — Practical Backend Examples
- Node.js / Express
Success — POST (201 Created)
app.post("/users", async (req, res) => {
const data = await createUserSchema.parseAsync(req.body);
const user = await prisma.user.create({ data });
res
.header("Location", `/users/${user.id}`)
.status(201)
.json(user);
});
Client Error — 422 Unprocessable Entity (preferred over 400 for field validation)
app.post("/users", async (req, res) => {
try {
const data = await createUserSchema.parseAsync(req.body);
// ...
} catch (err) {
if (err instanceof ZodError) {
return res.status(422).json({
message: "Validation failed",
errors: err.flatten().fieldErrors,
});
}
throw err;
}
});
409 Conflict — business rule violation
if (existingSubscription?.status === "active") {
return res.status(409).json({
code: "SUBSCRIPTION_ALREADY_ACTIVE",
message: "Cannot create trial — user already has active plan",
});
}
Frontend — Handling Status Codes Intelligently (React + Next.js)
- Next.js App Router + fetch
Global fetch wrapper with auto-logout on 401
"use client";
let isRefreshing = false;
export async function apiFetch(
input: RequestInfo | URL,
init?: RequestInit
): Promise<Response> {
const res = await fetch(input, {
...init,
credentials: "include",
headers: {
"Content-Type": "application/json",
...init?.headers,
},
});
if (res.status === 401 && !isRefreshing) {
isRefreshing = true;
try {
// attempt silent refresh
const refreshRes = await fetch("/api/auth/refresh", { method: "POST" });
if (!refreshRes.ok) throw new Error("refresh failed");
// retry original request
return apiFetch(input, init);
} catch {
// redirect to login or show session expired modal
window.location.href = "/login?session_expired=1";
} finally {
isRefreshing = false;
}
}
if (!res.ok) {
const errorData = await res.json().catch(() => ({}));
throw new ApiError(res.status, errorData.message || "Request failed", errorData);
}
return res;
}
class ApiError extends Error {
constructor(
public status: number,
message: string,
public data?: any
) {
super(message);
}
}
Using in Server Component vs Client Component
- Server Component → use
statusdirectly in RSC (no throwing → error.tsx) - Client Component → throw →
error.tsxoruseToast/ modal
Security & Best Practices (2026)
Status Code Checklist Before Shipping
-
Never return 200 with
{ success: false }— use real 4xx codes -
Use 201 + Location header on resource creation
-
Use 204 for successful DELETE / bulk updates without body
-
Prefer 422 over 400 for semantic/form validation errors
-
Return consistent error shape on 4xx/5xx
{
"status": 422,
"code": "VALIDATION_ERROR",
"message": "Invalid input",
"errors": { "email": ["Invalid email format"] }
} -
Include Retry-After on 429 and 503
-
Never leak stack traces / internal messages in 500 responses
-
Log 5xx with correlation ID — return generic "Something went wrong"
Common Anti-Patterns to Avoid
| Anti-pattern | Why it's bad | Better choice |
|---|---|---|
200 + { error: "..." } | Confuses caching, monitoring, interceptors | 4xx family |
| 404 for permission denied | Hides existence — security through obscurity | 403 Forbidden |
| 500 for validation errors | Misleads monitoring & alerting | 400 / 422 |
| No body on 400/401/403/422 | Client cannot show specific message | Always JSON body |
Glossary
-
1xx Informational — Rare in APIs (100 Continue mostly for large uploads)
-
2xx Success — Request completed as expected
-
3xx Redirection — Client should take additional action (mostly handled by browser)
-
4xx Client Error — Something wrong with the request (you fix it)
-
5xx Server Error — Something wrong on our side (we fix it)
-
Idempotent — Repeating the request produces the same result (GET, PUT, DELETE usually are)
Conclusion
HTTP status codes are not decoration — they are the first and most reliable part of the contract between frontend and backend. Using them correctly reduces conditional logic in your React components, improves debugging speed, makes monitoring meaningful, and aligns your application with how the web was designed to work.
Master them — and your APIs will feel professional, predictable, and much easier to integrate with.
Happy coding!
