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/abcdoesn'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.txtRemember
- 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.