Back to blog
Backend Systemsbeginner

Stop Returning 200 for Everything — HTTP Status Done Right

The complete guide to HTTP status codes in ASP.NET Core. Know exactly when to return 200, 201, 204, 400, 401, 403, 404, 409, 422, and 500 — and how to wire them up correctly.

LearnixoApril 14, 20264 min read
.NETC#RESTHTTPASP.NET Core
Share:𝕏

One of the easiest ways to tell a junior API from a senior one: check what status codes it returns. If everything comes back as 200 OK — even errors — that's a problem.

HTTP status codes are part of the API contract. They tell clients what happened without them needing to parse the body.


The Groups

| Range | Meaning | |-------|---------| | 2xx | Success | | 3xx | Redirect | | 4xx | Client made a mistake | | 5xx | Server made a mistake |


The Codes You'll Actually Use

200 OK

Success with a response body. Use for GET, PUT, PATCH.

C#
[HttpGet("{id}")]
public async Task<ActionResult<OrderDto>> GetById(int id, CancellationToken ct)
{
    var order = await _db.Orders.FindAsync(id, ct);
    if (order is null) return NotFound();
    return Ok(new OrderDto(order));
}

201 Created

Resource was created. Always include the new resource in the body and a Location header pointing to it.

C#
[HttpPost]
public async Task<IActionResult> Create(CreateOrderDto dto, CancellationToken ct)
{
    var order = await _orderService.CreateAsync(dto, ct);
    return CreatedAtAction(nameof(GetById), new { id = order.Id }, order);
    // Sets Location: /api/orders/42
}

204 No Content

Success, nothing to return. Use for DELETE and PUT/PATCH when you don't return the updated resource.

C#
[HttpDelete("{id}")]
public async Task<IActionResult> Delete(int id, CancellationToken ct)
{
    var order = await _db.Orders.FindAsync(id, ct);
    if (order is null) return NotFound();
    _db.Orders.Remove(order);
    await _db.SaveChangesAsync(ct);
    return NoContent();
}

400 Bad Request

The client sent malformed input — wrong types, missing required fields, format errors.

C#
[HttpPost]
public IActionResult Create(CreateOrderDto dto)
{
    if (!ModelState.IsValid)
        return BadRequest(ModelState);  // or ValidationProblem(ModelState)
    // ...
}

With FluentValidation this happens automatically via the pipeline behavior.

401 Unauthorized

The request has no valid authentication. No token, expired token, invalid signature.

C#
// ASP.NET Core returns 401 automatically when [Authorize] is used and no valid token is present
[Authorize]
[HttpGet("profile")]
public IActionResult GetProfile() => Ok(_currentUser.Profile);

403 Forbidden

Authenticated but not allowed. The user is known but doesn't have permission.

C#
[Authorize(Roles = "Admin")]
[HttpDelete("{id}")]
public IActionResult Delete(int id)
{
    // Returns 403 if authenticated user is not in Admin role
}

The difference matters: 401 = "who are you?", 403 = "I know who you are, but no."

404 Not Found

The resource doesn't exist.

C#
var order = await _db.Orders.FindAsync(id, ct);
if (order is null)
    return NotFound();  // or NotFound(new { message = $"Order {id} not found" })

409 Conflict

The request is valid but conflicts with current state — duplicate email, optimistic concurrency violation, trying to cancel an already-shipped order.

C#
var existing = await _db.Users.FirstOrDefaultAsync(u => u.Email == dto.Email, ct);
if (existing is not null)
    return Conflict(new { message = "Email already registered." });

422 Unprocessable Entity

Valid input format, but business rules say no. Different from 400 — the structure is fine, but the action can't be performed.

C#
var order = await _db.Orders.FindAsync(id, ct);
if (order.Status == OrderStatus.Shipped)
    return UnprocessableEntity(new { message = "Cannot cancel a shipped order." });

500 Internal Server Error

Something unexpected went wrong on the server. Never return this manually — let the global exception handler do it.

C#
// Program.cs — global exception handler returns 500 automatically
app.UseExceptionHandler("/error");
// or
app.UseExceptionHandler();  // with ProblemDetails middleware

Quick Cheat Sheet

GET    success                 → 200
POST   success (created)       → 201 + Location header
PUT    success                 → 200 (with body) or 204
PATCH  success                 → 200 (with body) or 204
DELETE success                 → 204

Bad input / validation         → 400
Not authenticated              → 401
Authenticated, no permission   → 403
Resource not found             → 404
State conflict / duplicate     → 409
Business rule violation        → 422
Server crash                   → 500 (automatic)

Wire Up ProblemDetails

All error responses should use RFC 9457 ProblemDetails format, not raw strings.

C#
// Program.cs
builder.Services.AddProblemDetails();
app.UseExceptionHandler();
app.UseStatusCodePages();

Now NotFound(), BadRequest(), Conflict() etc. all return:

JSON
{
  "type": "https://tools.ietf.org/html/rfc9110#section-15.5.5",
  "title": "Not Found",
  "status": 404,
  "detail": "Order 42 was not found.",
  "instance": "/api/orders/42"
}

Consistent across every endpoint, without any extra code.

Enjoyed this article?

Explore the Backend Systems learning path for more.

Found this helpful?

Share:𝕏

Leave a comment

Have a question, correction, or just found this helpful? Leave a note below.