Updated October 2026 for .NET 10 (LTS) and ASP.NET Core 10, with controller and minimal API examples.

Returning the right HTTP status code tells API clients exactly what happened: success, a bad request, a missing resource or a server failure. ASP.NET Core has a helper method for every common status code, and built-in support for standard error responses (Problem Details). This guide shows both controllers and minimal APIs.

Quick reference

StatusMeaningControllerMinimal API
200 OKSuccess with a bodyOk(item)TypedResults.Ok(item)
201 CreatedNew resource createdCreatedAtAction(...)TypedResults.Created(uri, item)
204 No ContentSuccess, no bodyNoContent()TypedResults.NoContent()
400 Bad RequestInvalid inputBadRequest() / ValidationProblem()TypedResults.ValidationProblem(errors)
401 UnauthorizedNot signed inUnauthorized()TypedResults.Unauthorized()
403 ForbiddenSigned in, not allowedForbid()TypedResults.Forbid()
404 Not FoundResource does not existNotFound()TypedResults.NotFound()
409 ConflictClashes with current stateConflict()TypedResults.Conflict()
500 / 503Server-side failureProblem(...)TypedResults.Problem(...)

For any other code, use StatusCode(StatusCodes.Status418ImATeapot) in a controller or TypedResults.StatusCode(...) in a minimal API. The StatusCodes class has a named constant for every standard code.

Controllers: return ActionResult<T>

[ApiController]
[Route("api/[controller]")]
public class BooksController(IBookService books) : ControllerBase
{
    [HttpGet("{id}")]
    [ProducesResponseType<Book>(StatusCodes.Status200OK)]
    [ProducesResponseType(StatusCodes.Status404NotFound)]
    public async Task<ActionResult<Book>> Get(string id)
    {
        var book = await books.GetAsync(id);
        return book is null ? NotFound() : Ok(book);
    }

    [HttpPost]
    [ProducesResponseType<Book>(StatusCodes.Status201Created)]
    [ProducesResponseType(StatusCodes.Status409Conflict)]
    public async Task<ActionResult<Book>> Create(Book book)
    {
        if (await books.ExistsAsync(book.Id))
        {
            return Conflict();
        }

        await books.AddAsync(book);
        return CreatedAtAction(nameof(Get), new { id = book.Id }, book); // 201 + Location header
    }

    [HttpDelete("{id}")]
    [ProducesResponseType(StatusCodes.Status204NoContent)]
    [ProducesResponseType(StatusCodes.Status404NotFound)]
    public async Task<IActionResult> Delete(string id) =>
        await books.DeleteAsync(id) ? NoContent() : NotFound();
}

With [ApiController], invalid model input returns 400 automatically, and error codes such as 404 get a standard Problem Details body. The ProducesResponseType attributes document each code in your OpenAPI/Swagger output.

Minimal APIs: use TypedResults

In minimal APIs, return TypedResults and declare the possible results with Results<...>. The compiler checks your code, and OpenAPI picks up every status code automatically:

using Microsoft.AspNetCore.Http.HttpResults;

app.MapGet("/books/{id}", async Task<Results<Ok<Book>, NotFound>> (string id, IBookService books) =>
    await books.GetAsync(id) is { } book
        ? TypedResults.Ok(book)
        : TypedResults.NotFound());

app.MapPost("/books", async Task<Results<Created<Book>, Conflict>> (Book book, IBookService books) =>
{
    if (await books.ExistsAsync(book.Id))
    {
        return TypedResults.Conflict();
    }

    await books.AddAsync(book);
    return TypedResults.Created($"/books/{book.Id}", book);
});

Errors: return Problem Details, not plain text

Problem Details is the standard JSON format for HTTP API errors (RFC 9457, which replaced RFC 7807). Clients get a consistent body with type, title, status, detail and instance, served as application/problem+json. Return one directly:

return Problem(
    title: "Payment service unavailable",
    detail: "Please try again in a few minutes.",
    statusCode: StatusCodes.Status503ServiceUnavailable);

Handle unexpected exceptions in one place

Do not wrap every action in try/catch just to return 500. Register a global exception handler once, and every unhandled exception becomes a clean Problem Details response:

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddControllers();
builder.Services.AddProblemDetails();
builder.Services.AddExceptionHandler<GlobalExceptionHandler>();

var app = builder.Build();

app.UseExceptionHandler();   // unhandled exceptions -> Problem Details
app.UseStatusCodePages();    // empty error responses -> Problem Details

app.MapControllers();
app.Run();
using Microsoft.AspNetCore.Diagnostics;
using Microsoft.AspNetCore.Mvc;

public sealed class GlobalExceptionHandler(
    IProblemDetailsService problemDetails,
    ILogger<GlobalExceptionHandler> logger) : IExceptionHandler
{
    public async ValueTask<bool> TryHandleAsync(
        HttpContext httpContext, Exception exception, CancellationToken cancellationToken)
    {
        logger.LogError(exception, "Unhandled exception");

        var status = exception switch
        {
            ArgumentException => StatusCodes.Status400BadRequest,
            KeyNotFoundException => StatusCodes.Status404NotFound,
            _ => StatusCodes.Status500InternalServerError
        };

        httpContext.Response.StatusCode = status;

        return await problemDetails.TryWriteAsync(new ProblemDetailsContext
        {
            HttpContext = httpContext,
            Exception = exception,
            ProblemDetails = new ProblemDetails
            {
                Status = status,
                Title = status == 500 ? "An unexpected error occurred" : exception.Message
            }
        });
    }
}

Never send stack traces or internal messages to clients for 500 errors. Log them on the server, as above.

FAQ

200 or 204 for a successful update?

Return 200 with the updated resource if clients need it, or 204 if there is nothing to send back. Both are correct; be consistent across your API.

401 or 403?

401 means “we don’t know who you are” (missing or invalid credentials). 403 means “we know who you are, but you are not allowed to do this”.

Related

Sources: Microsoft Learn: Controller action return types · Microsoft Learn: Minimal API responses · Microsoft Learn: Handle errors in ASP.NET Core · RFC 9457 · RFC 9110

2 thoughts on “Return HTTP Status Codes in ASP.NET Core (.NET 10): Controllers, Minimal APIs and Problem Details”

Leave a Reply

Your email address will not be published. Required fields are marked *