ASP.NET Core Web APIs · Minimal APIs Done Right
Minimal APIs, Route Groups and Typed Results
Build a JSON API with ASP.NET Core minimal APIs: route groups, route parameters, TypedResults and correct status codes.
What you'll build
A /api/products API with list, get, and create endpoints in a route group, returning TypedResults (200, 201 with Location, 404). Every example in this course is a complete .NET 10 Program.cs that starts the app on a free port and calls it with HttpClient.
The idea
WebApplication.CreateBuilder()sets up configuration, logging and dependency injection;builder.Build()creates the app.app.MapGet/MapPost(...)maps routes to lambdas; parameters bind from the route, query, body and services automatically.- Route groups (
app.MapGroup("/api/products")) share a prefix and conventions (filters, auth, tags). TypedResults(Ok,Created,NotFound) return correct status codes and describe responses for OpenAPI.
The code
using Microsoft.AspNetCore.Http.HttpResults;
var app = WebApplication.Create();
var products = new List<Product> { new(1, "Mug", 8.50m), new(2, "Teapot", 24.00m) };
var group = app.MapGroup("/api/products");
group.MapGet("/", () => TypedResults.Ok(products));
group.MapGet("/{id:int}", Results<Ok<Product>, NotFound> (int id) =>
products.FirstOrDefault(p => p.Id == id) is { } p ? TypedResults.Ok(p) : TypedResults.NotFound());
group.MapPost("/", (ProductInput input) =>
{
var product = new Product(products.Max(p => p.Id) + 1, input.Name, input.Price);
products.Add(product);
return TypedResults.Created($"/api/products/{product.Id}", product);
});
app.Urls.Add("http://127.0.0.1:0");
await app.StartAsync();
using var http = new HttpClient { BaseAddress = new Uri(app.Urls.First()) };
await Show(await http.GetAsync("/api/products/2"), "GET /api/products/2");
await Show(await http.GetAsync("/api/products/9"), "GET /api/products/9");
var created = await http.PostAsJsonAsync("/api/products", new ProductInput("Kettle", 31.90m));
Console.WriteLine($"Location: {created.Headers.Location}");
await Show(created, "POST /api/products");
await app.StopAsync();
static async Task Show(HttpResponseMessage r, string label)
{
var body = await r.Content.ReadAsStringAsync();
Console.WriteLine($"{label} -> {(int)r.StatusCode} {body}");
}
record Product(int Id, string Name, decimal Price);
record ProductInput(string Name, decimal Price);
Output
GET /api/products/2 -> 200 {"id":2,"name":"Teapot","price":24.00}
GET /api/products/9 -> 404
Location: /api/products/3
POST /api/products -> 201 {"id":3,"name":"Kettle","price":31.90}Run this on your computer — see “Get Started” for setup.
Common mistakes
- Returning
Results.Ok(null)for missing items instead of 404. - Putting everything in
Program.csas the API grows; move endpoint groups into extension methods (app.MapProductEndpoints()). - Using
IResulteverywhere, losing response type information for OpenAPI; preferTypedResultsandResults<...>unions.
Key points
- Minimal APIs map routes to handlers with automatic parameter binding.
- Route groups share prefixes and conventions.
- TypedResults give correct status codes and accurate OpenAPI metadata.
Further reading: Minimal APIs overview · Route groups
Practice questions
What's the advantage of TypedResults over Results in minimal APIs?
Answer
TypedResults return concrete types (Ok<T>, NotFound) that describe responses for OpenAPI and make handlers easier to unit test.
What does app.MapGroup do?
Answer
Creates a group of endpoints sharing a route prefix and conventions such as filters, authorization and OpenAPI tags.
How does {id:int} in a route template help?
Answer
The route constraint matches only integers and binds the value as an int; other values don't match that endpoint.
Open in the app to mark lessons complete, take notes and track your progress.