Routing and minimal API endpoints

ASP.NET Core · Essentials · 12 min

Routes are the public surface of your API; designing and matching them correctly is step one.

Minimal APIs map a route pattern and HTTP method directly to a handler, usually a lambda:

app.MapGet("/todos/{id}", (int id) => ...);

There is a method per verb: MapGet, MapPost, MapPut, MapPatch, MapDelete, plus MapMethods for others. The handler can also be a method group (app.MapGet("/x", Handlers.GetX)), which keeps Program.cs short as the app grows.

Route templates:

  • {id} captures a segment; a handler parameter with the same name receives it, converted to its type. A non-convertible value gives a 400 (for minimal APIs) rather than calling the handler.
  • Constraints restrict matches: {id:int}, {slug:alpha}, {n:min(1)}, {code:length(3)}, {date:datetime}. With a constraint, /todos/abc doesn't match at all (404) so another route can take it.
  • Optional {id?}, defaults {page=1}, and catch-all {*path}.

Routing picks the most specific matching endpoint regardless of declaration order: a literal segment like /todos/today beats /todos/{id}. Ambiguous matches raise an exception at request time.

Name endpoints with .WithName("GetTodo") so you can generate links to them, and use route groups (app.MapGroup("/todos"), Advanced level) to share prefixes and settings. Keep handlers thin: they should parse input, call a service and shape the response.

Example

var app = WebApplication.Create(args);

var todos = new Dictionary<int, string> { [1] = "Learn routing", [2] = "Write tests" };

app.MapGet("/todos", () => todos.Values);
app.MapGet("/todos/today", () => "literal routes win over parameters");
app.MapGet("/todos/{id:int}", (int id) =>
    todos.TryGetValue(id, out var t) ? Results.Ok(t) : Results.NotFound());
app.MapPost("/todos", (string title) =>
{
    var id = todos.Keys.Max() + 1;
    todos[id] = title;
    return Results.Created($"/todos/{id}", title);
});
app.MapDelete("/todos/{id:int}", (int id) => todos.Remove(id) ? Results.NoContent() : Results.NotFound());
app.MapGet("/files/{*path}", (string path) => $"file: {path}");

app.Run();
// GET /todos/1        -> "Learn routing"
// GET /todos/abc      -> 404 (int constraint does not match)
// GET /todos/today    -> literal routes win over parameters
// POST /todos?title=Ship  -> 201 Created, Location: /todos/3
// GET /files/a/b.txt  -> file: a/b.txt

Remember

  • MapGet/MapPost/... bind a verb and a route template to a handler
  • {name} segments bind to same-named handler parameters
  • Constraints like {id:int} make non-matching URLs fall through to 404
  • The most specific route wins, not the first declared

Watch out for

  • Relying on declaration order to pick between overlapping routes; ASP.NET Core ranks by specificity instead.

Check yourself

  1. With routes /items/{id:int} and no other match, what does GET /items/abc return?

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/routing