Skip to main content

HTTP Response Status Codes — Practical Guide for Modern Web & API Development

· 5 min read
Mezaache Akram
Mezaache Akram
Developer & Frontend team lead

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.

Laptop screen showing HTTP response codes and network tab

Photo by ThisIsEngineering on Unsplash

Quick Reference — Most Used Status Codes in 2026

CodeMeaningTypical Use Case (Frontend + Backend)Body?Idempotent?
200OKGET — resource returned, PUT/PATCH — updatedYesYes
201CreatedPOST — new resource created, Location header often usedYesNo
204No ContentDELETE successful, PUT/PATCH when no body neededNoYes
301Moved PermanentlyPermanent URL redirection (SEO important)SometimesYes
400Bad RequestValidation failed, malformed JSON, missing required fieldYes
401UnauthorizedMissing/invalid token, session expiredYes
403ForbiddenAuthenticated but not allowed (role/permission)Yes
404Not FoundResource does not existYes/No
409ConflictBusiness rule violation (e.g. cannot delete paid invoice)Yes
422Unprocessable EntitySemantic validation failed (WebDAV origin, very common now)Yes
429Too Many RequestsRate limiting hitYes
500Internal Server ErrorUnexpected crash — never expose stack traceYes
502Bad GatewayProxy / load balancer issueSometimes
503Service UnavailableMaintenance, overload — should include Retry-AfterYes

Most Important Status Codes — Practical Backend Examples

Success — POST (201 Created)

routes/users.ts
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)

Global fetch wrapper with auto-logout on 401

lib/api.ts
"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 status directly in RSC (no throwing → error.tsx)
  • Client Component → throw → error.tsx or useToast / 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-patternWhy it's badBetter choice
200 + { error: "..." }Confuses caching, monitoring, interceptors4xx family
404 for permission deniedHides existence — security through obscurity403 Forbidden
500 for validation errorsMisleads monitoring & alerting400 / 422
No body on 400/401/403/422Client cannot show specific messageAlways 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!