HTTP Status Codes
Status codes are how an API tells a client what happened before it even reads the body. Good codes let the React app show the right message, the mobile app decide whether to retry, and monitoring count real errors. ASP.NET Core also formats errors as standard problem details.
On this page · 7 min · Beginner
On this page
- In simple words
- Learning objectives
- Explanation
- Syntax
- Code example
- Step by step
- Real-world use
- Common mistakes
- Best practices
- Remember this
- Practice exercise
- Mini quiz
- Related topics
- My notes
- Reading time
- 7 min
- Difficulty
- Beginner
- Updated
- Oct 29, 2026
Related tutorials
In simple words#
Status codes are the API’s facial expressions: 2xx smile (OK), 4xx “you made a mistake”, 5xx “I made a mistake”.
- 200 / 201 / 204
- OK / Created / No content.
- 400 / 401 / 403 / 404
- Bad input / not logged in / not allowed / not found.
- 500
- Server error.
Learning objectives#
- Choose the right 2xx code for each success
- Choose between 400, 404, 409 and 422 for client errors
- Return problem details (RFC 9457) for every error
- Turn unhandled exceptions into a clean 500
Explanation#
Success: 200 OK (data returned), 201 Created (new resource + Location), 202 Accepted (work queued), 204 No Content (done, nothing to return).
Client errors: 400 Bad Request (malformed or invalid input), 401 Unauthorized (not logged in), 403 Forbidden (logged in, not allowed), 404 Not Found, 409 Conflict (duplicate, version conflict), 422 Unprocessable Entity (valid input but a business rule fails), 429 Too Many Requests.
Server errors: 500 for bugs and unexpected failures, 503 when a dependency is down. Never use 200 with { "success": false }.
builder.Services.AddProblemDetails() makes error responses use application/problem+json with type, title, status, detail and a traceId. In controllers, NotFound(), Conflict(), ValidationProblem() and Problem(...) produce them.
app.UseExceptionHandler() together with AddProblemDetails turns unhandled exceptions into a 500 problem response without leaking the stack trace.
Syntax#
return Ok(dto); // 200
return CreatedAtAction(nameof(Get), new { id }, dto); // 201
return NoContent(); // 204
return NotFound(); // 404 problem details
return Conflict(new ProblemDetails { Title = "SKU already exists" }); // 409
return Problem(statusCode: 422, title: "Not enough stock"); // 422
Code example: Every common outcome and its status code#
// Program.cs
using System.Net.Http.Json;
using System.Text.Json;
var builder = WebApplication.CreateBuilder(args);
builder.Logging.ClearProviders(); // keep the demo output clean
builder.Services.AddControllers();
builder.Services.AddProblemDetails(); // RFC 9457 error bodies
var app = builder.Build();
app.UseExceptionHandler(); // unhandled exceptions → 500 problem details
app.MapControllers();
app.Urls.Add("http://127.0.0.1:5204");
await app.StartAsync();
using var http = new HttpClient { BaseAddress = new Uri("http://127.0.0.1:5204") };
async Task Show(string label, Task<HttpResponseMessage> call)
{
var response = await call;
var body = await response.Content.ReadAsStringAsync();
var summary = response.Content.Headers.ContentType?.MediaType == "application/problem+json"
? quot;problem: {JsonDocument.Parse(body).RootElement.GetProperty("title").GetString()}"
: body;
Console.WriteLine(quot;{label,-28} {(int)response.StatusCode} {summary}".TrimEnd());
}
await Show("GET existing order", http.GetAsync("/api/orders/1"));
await Show("POST valid order", http.PostAsJsonAsync("/api/orders", new { sku = "MOUSE-1", quantity = 2 }));
await Show("POST invalid body", http.PostAsJsonAsync("/api/orders", new { sku = "", quantity = 0 }));
await Show("POST too many items", http.PostAsJsonAsync("/api/orders", new { sku = "HUB-2", quantity = 50 }));
await Show("POST duplicate request id", http.PostAsJsonAsync("/api/orders", new { sku = "MOUSE-1", quantity = 1, requestId = "abc" }));
await Show("POST duplicate request id", http.PostAsJsonAsync("/api/orders", new { sku = "MOUSE-1", quantity = 1, requestId = "abc" }));
await Show("GET missing order", http.GetAsync("/api/orders/99"));
await Show("DELETE order", http.DeleteAsync("/api/orders/1"));
await Show("GET report (bug inside)", http.GetAsync("/api/orders/report"));
await app.StopAsync();
// Controllers/OrdersController.cs
using System.ComponentModel.DataAnnotations;
using Microsoft.AspNetCore.Mvc;
public record OrderDto(int Id, string Sku, int Quantity);
public record PlaceOrderDto([Required] string Sku, [Range(1, 100)] int Quantity, string? RequestId);
[ApiController]
[Route("api/[controller]")]
public class OrdersController : ControllerBase
{
private static readonly Dictionary<int, OrderDto> Orders = new() { [1] = new(1, "HUB-2", 1) };
private static readonly HashSet<string> SeenRequestIds = [];
private static readonly Dictionary<string, int> Stock = new() { ["MOUSE-1"] = 10, ["HUB-2"] = 5 };
[HttpGet("{id:int}")]
public ActionResult<OrderDto> Get(int id) => Orders.TryGetValue(id, out var order) ? Ok(order) : NotFound();
[HttpPost]
public ActionResult<OrderDto> Place(PlaceOrderDto dto)
{
if (dto.RequestId is not null && !SeenRequestIds.Add(dto.RequestId))
return Conflict(new ProblemDetails { Title = "This request was already processed" }); // 409
if (Stock.GetValueOrDefault(dto.Sku) < dto.Quantity)
return Problem(statusCode: StatusCodes.Status422UnprocessableEntity, title: "Not enough stock"); // 422
var order = new OrderDto(Orders.Keys.Max() + 1, dto.Sku, dto.Quantity);
Orders[order.Id] = order;
return CreatedAtAction(nameof(Get), new { id = order.Id }, order); // 201
}
[HttpDelete("{id:int}")]
public IActionResult Cancel(int id) => Orders.Remove(id) ? NoContent() : NotFound(); // 204 / 404
[HttpGet("report")]
public IActionResult Report() => throw new InvalidOperationException("Reporting database is offline"); // → 500
}
GET existing order 200 {"id":1,"sku":"HUB-2","quantity":1}
POST valid order 201 {"id":2,"sku":"MOUSE-1","quantity":2}
POST invalid body 400 problem: One or more validation errors occurred.
POST too many items 422 problem: Not enough stock
POST duplicate request id 201 {"id":3,"sku":"MOUSE-1","quantity":1}
POST duplicate request id 409 problem: This request was already processed
GET missing order 404 problem: Not Found
DELETE order 204
GET report (bug inside) 500 problem: An error occurred while processing your request.This example is illustrative. Copy or download it and run it in your own environment; the expected output is shown above.
Step-by-step explanation#
- Successful reads return 200 and new orders 201 with a Location header.
- An invalid body never reaches the action: [ApiController] answers 400 with a validation problem.
- Valid input that breaks a business rule (not enough stock) is a 422; a repeated request id is a 409 conflict.
- Deleting returns 204 No Content; a missing order is a 404 problem.
- The exception becomes a 500 problem details response — the message “Reporting database is offline” is not sent to the client.
Real-world use case#
ShopSmart’s React checkout shows field errors for 400, “out of stock” for 422, “already submitted” for 409, retries automatically on 503, and reports 500s to the support team with the traceId from the problem details.
Common mistakes#
{ "success": false } with status 200 breaks caching, retries and monitoring. Use 4xx/5xx.
Bad input is a 400 or 422, not a server error. Otherwise alerts fire for problems that are not yours.
Exception details may contain SQL or internal names. Log them; return a generic 500 problem.
Best practices#
- AddProblemDetails + UseExceptionHandler
- 201 with Location, 204 for no content
- 409 for conflicts, 422 for rule violations
- Document error responses in OpenAPI
Remember this#
2xx success · 4xx client · 5xx server.
400 bad input · 404 missing · 409 conflict · 422 rule broken.
Errors as application/problem+json.
Practice exercise#
Your task: Add an action POST /api/orders/{id}/ship that returns 404 for unknown orders, 409 if the order was already shipped, and 202 Accepted otherwise (shipping happens later).
Need a hint?
Show solution
// Controllers/ShippingController.cs
using Microsoft.AspNetCore.Mvc;
[ApiController]
[Route("api/orders")]
public class ShippingController : ControllerBase
{
private static readonly HashSet<int> ExistingOrders = [1, 2, 3];
private static readonly HashSet<int> Shipped = [];
[HttpPost("{id:int}/ship")]
public IActionResult Ship(int id)
{
if (!ExistingOrders.Contains(id)) return NotFound();
if (!Shipped.Add(id)) return Conflict(new ProblemDetails { Title = "Order already shipped" });
return Accepted();
}
}
Mini quiz#
A customer orders 50 items but only 5 are in stock. The request body itself is valid. Which status fits best?
Want more? Practise all ASP.NET Core Web API MCQs · ASP.NET Core Web API interview questions
My notes#
Saved in this browser.