Responses and status codes

ASP.NET Core · Essentials · 10 min

Clients depend on correct status codes and response shapes; this is your API contract.

What a handler returns becomes the response:

  • A string is written as text/plain with 200.
  • Any other object (record, list, anonymous type) is serialized to JSON with 200.
  • An IResult takes full control: status code, headers and body.

The Results and TypedResults static classes create IResults: Ok(value), Created(uri, value), NoContent(), BadRequest(error), NotFound(), Conflict(), Problem(...), ValidationProblem(errors), Unauthorized(), Forbid(), Redirect(url), File(bytes, contentType), Stream(...), StatusCode(429).

Prefer TypedResults: they return concrete types like Ok<Todo>, so the handler's return type documents its outcomes and OpenAPI can describe them automatically. When a handler can produce several outcomes, declare a union return type:

static Results<Ok<Todo>, NotFound> Get(int id) => ...

Each branch returns a different TypedResult, and the compiler checks that only the declared results are used.

Async handlers return Task<T> and work the same way. Use proper semantics: 201 with a Location header for creation, 204 for successful deletes without a body, 404 for missing resources, 400/422 for invalid input, 409 for conflicts.

JSON options (naming policy, enum as strings) are configured with builder.Services.ConfigureHttpJsonOptions(o => ...).

Example

using Microsoft.AspNetCore.Http.HttpResults;
using System.Text.Json.Serialization;

var builder = WebApplication.CreateBuilder(args);
builder.Services.ConfigureHttpJsonOptions(o =>
    o.SerializerOptions.Converters.Add(new JsonStringEnumConverter()));
var app = builder.Build();

var todos = new List<Todo> { new(1, "Learn results", Status.Open) };

app.MapGet("/todos/{id}", Results<Ok<Todo>, NotFound> (int id) =>
    todos.Find(t => t.Id == id) is { } todo ? TypedResults.Ok(todo) : TypedResults.NotFound());

app.MapPost("/todos", Created<Todo> (Todo todo) =>
{
    todos.Add(todo);
    return TypedResults.Created($"/todos/{todo.Id}", todo);
});
app.MapGet("/ping", () => "pong");          // text/plain
app.Run();
// GET /todos/1 -> 200 {"id":1,"title":"Learn results","status":"Open"}
// GET /todos/9 -> 404

enum Status { Open, Done }
record Todo(int Id, string Title, Status Status);

Remember

  • Objects become JSON, strings become text/plain
  • Return IResult for status codes and headers
  • TypedResults + Results<A, B> make outcomes explicit and documentable
  • Use the right status code for each outcome

Check yourself

  1. Why prefer TypedResults.Ok(todo) over Results.Ok(todo)?

Sign in to answer, see the explanations, and get this lesson back as a quick review later.

Official docs: learn.microsoft.com/en-us/aspnet/core/fundamentals/minimal-apis/responses