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.

Create a free account to save this page, take notes and track your progress. Sign in

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

A route group with typed results
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.cs as the API grows; move endpoint groups into extension methods (app.MapProductEndpoints()).
  • Using IResult everywhere, losing response type information for OpenAPI; prefer TypedResults and Results<...> 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.

Was this page helpful?