ASP.NET Core Web API · REST Foundations

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.

Beginner 7 min Lesson 4 of 37Examples target .NET 9.
Before you start:HTTP Methods
0/37 in ASP.NET Core Web API
On this page · 7 min · Beginner

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#

C# Illustrative
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#

C#Controllers/OrdersController.cs Illustrative
// 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 }
Expected output
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#

  1. Successful reads return 200 and new orders 201 with a Location header.
  2. An invalid body never reaches the action: [ApiController] answers 400 with a validation problem.
  3. Valid input that breaks a business rule (not enough stock) is a 422; a repeated request id is a 409 conflict.
  4. Deleting returns 204 No Content; a missing order is a 404 problem.
  5. 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#

200 for errors

{ "success": false } with status 200 breaks caching, retries and monitoring. Use 4xx/5xx.

500 for client mistakes

Bad input is a 400 or 422, not a server error. Otherwise alerts fire for problems that are not yours.

Leaking exception messages

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?
Keep a HashSet<int> of shipped ids; return Accepted() for success.
Show solution
C# Illustrative
// 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#

HTTP Status Codes quizMultiple choice Question 1 of 3

A customer orders 50 items but only 5 are in stock. The request body itself is valid. Which status fits best?

Score so far: 0/0

Want more? Practise all ASP.NET Core Web API MCQs · ASP.NET Core Web API interview questions

My notes#

Saved in this browser.

Progress is saved in this browser. Sign in to sync.