← Open in the full interactive course (progress tracking, search & more)

You already wrote these. Now let's formally learn what they actually are.

In lessons 161 and 162, you wrote things like app.MapGet("/tasks/{id}", (int id, ITaskRepository repo) => repo.GetById(id)). It worked. Route parameters got matched up, a repository got handed to you out of nowhere, and JSON came back to the caller — all without a single class, base type, or attribute in sight. You were using Minimal APIs, pragmatically, before this course had formally introduced the term.

This lesson makes that formal. You'll learn exactly what mechanism binds route parameters and services into your delegate's parameter list, how to group related routes cleanly, how to hook cross-cutting logic into specific endpoints with IEndpointFilter, and — just as importantly — an honest answer to when this style is the right call and when it starts to strain.

In this lesson, you'll learn MapGet/MapPost/MapPut/MapDelete, how dependency injection reaches directly into a handler's parameters with no constructor involved, endpoint filters, and grouping related routes with MapGroup.

What Is It?

The Simple Explanation

Minimal APIs are a way of building HTTP endpoints in ASP.NET Core by mapping a route directly to a function — no controller class, no base class, no attributes required. You write app.MapGet("/products/{id}", ...), and the function you pass in is the entire handler for that route.

The Technical Definition

A Minimal API endpoint is registered by calling one of the Map... extension methods on WebApplication (or on an IEndpointRouteBuilder more generally) — MapGet, MapPost, MapPut, MapDelete, and others for the remaining HTTP verbs. Each call takes a route template and a delegate. Internally, ASP.NET Core inspects the delegate's parameter list via a mechanism called the request delegate factory, and generates code that extracts each parameter's value from the appropriate part of the incoming request — the route, the query string, the body, or the DI container — before invoking your delegate.

Why Does It Exist?

The Problem — Controllers Carry Ceremony Small APIs Don't Need

Classic MVC Controllers (lesson 257) require a fair amount of structure before you write your first line of business logic: a class, a base type, attribute routing, a project convention for where controllers live and how they're discovered. For a large, sprawling API with dozens of resources, that structure earns its keep. But for a small service — a single-purpose microservice with five endpoints, or a quick internal tool — that ceremony is pure overhead, adding boilerplate without adding clarity.

The Solution — Routes as Functions

Minimal APIs, introduced in .NET 6, strip that ceremony away. A route and its handling logic sit right next to each other, as a route template and a function. There's still a full framework underneath — the same DI container, the same middleware pipeline, the same model binding concepts from lesson 258 — but the amount of code required to expose one HTTP endpoint drops to almost nothing.

Big Picture

A MINIMAL API ENDPOINT, PIECE BY PIECE
app.MapPost("/orders/{customerId}", (int customerId, CreateOrderRequest body, IOrderService svc) => ...)
            │                        │              │                    │
     route template          from ROUTE      from REQUEST BODY     from DI CONTAINER
     (HTTP verb: POST)      (matches "{customerId}")   (complex type → JSON)   (registered service)

Every piece of a Minimal API's signature is doing double duty: it's both the ordinary C# parameter list and the declaration of where each value should come from. There's no separate configuration step — the shape of the function is the binding contract.

How It Works

THE Map* METHODS AND WHAT THEY BIND
1. MAP A VERB TO A ROUTE
app.MapGet("/orders/{id:int}", (int id) => ...);
app.MapPost("/orders", (CreateOrderRequest req) => ...);
app.MapPut("/orders/{id:int}", (int id, UpdateOrderRequest req) => ...);
app.MapDelete("/orders/{id:int}", (int id) => ...);
2. PARAMETERS BIND FROM THE MATCHING SOURCE
3. DI RESOLVES SERVICE-TYPED PARAMETERS DIRECTLY
app.MapGet("/orders/{id:int}", (int id, IOrderService orderService) =>
    orderService.GetById(id));
4. ADD ENDPOINT FILTERS FOR CROSS-CUTTING LOGIC
app.MapPost("/orders", (CreateOrderRequest req, IOrderService svc) => svc.Create(req))
   .AddEndpointFilter<ValidationFilter<CreateOrderRequest>>();
5. GROUP RELATED ROUTES WITH MapGroup
var orders = app.MapGroup("/api/orders").RequireAuthorization();

orders.MapGet("/{id:int}", (int id, IOrderService svc) => svc.GetById(id));
orders.MapPost("/", (CreateOrderRequest req, IOrderService svc) => svc.Create(req));

Simple Example

A small, complete CRUD surface for a Product record — using current C# idioms throughout:

public record Product(int Id, string Name, decimal Price);
public record CreateProductRequest(string Name, decimal Price);

var builder = WebApplication.CreateBuilder(args);
builder.Services.AddSingleton<IProductRepository, InMemoryProductRepository>();
var app = builder.Build();

var products = app.MapGroup("/api/products");

products.MapGet("/", (IProductRepository repo) => repo.GetAll());

products.MapGet("/{id:int}", (int id, IProductRepository repo) =>
    repo.GetById(id) is { } product ? Results.Ok(product) : Results.NotFound());

products.MapPost("/", (CreateProductRequest request, IProductRepository repo) =>
{
    var product = repo.Add(request.Name, request.Price);
    return Results.Created($"/api/products/{product.Id}", product);
});

products.MapPut("/{id:int}", (int id, CreateProductRequest request, IProductRepository repo) =>
    repo.Update(id, request.Name, request.Price) ? Results.NoContent() : Results.NotFound());

products.MapDelete("/{id:int}", (int id, IProductRepository repo) =>
    repo.Delete(id) ? Results.NoContent() : Results.NotFound());

app.Run();

What's happening in each line: id binds from the route segment. request/body-shaped parameters bind from JSON. repo is resolved by DI, exactly like a constructor parameter would be — except there's no constructor, because there's no class. Results.Ok, Results.NotFound, Results.Created, and Results.NoContent are Minimal API helpers that produce the right status code and, where relevant, serialize the body as JSON.

Real-World Example

Revisit the employee management API from lesson 162, now formalized with a group and an endpoint filter for validation — the kind of shape a small, focused microservice settles into:

public class LoggingFilter(ILogger<LoggingFilter> logger) : IEndpointFilter
{
    public async ValueTask<object?> InvokeAsync(
        EndpointFilterInvocationContext context, EndpointFilterDelegate next)
    {
        logger.LogInformation("Handling {Endpoint}", context.HttpContext.Request.Path);
        var result = await next(context);   // runs the rest of the filter chain, then the handler
        logger.LogInformation("Finished with result type {Type}", result?.GetType().Name);
        return result;
    }
}

var employees = app.MapGroup("/api/employees")
                    .AddEndpointFilter<LoggingFilter>()
                    .WithTags("Employees");

employees.MapGet("/{id:int}", (int id, IEmployeeService service) => service.GetById(id));
employees.MapPost("/", (CreateEmployeeRequest request, IEmployeeService service) =>
    Results.Created($"/api/employees", service.Create(request)));

Notice IEndpointFilter's shape — a next delegate you call to continue the chain, and code that can run both before and after that call. It's the same onion pattern from middleware (lesson 254) and from MVC filters (lesson 255), just scoped to one endpoint (or, here, one whole group of them) instead of the entire app or a whole Controller.

Under the Hood

HOW ASP.NET CORE TURNS A DELEGATE INTO AN ENDPOINT
1. THE REQUEST DELEGATE FACTORY INSPECTS YOUR SIGNATURE
2. IT'S STILL A NORMAL RequestDelegate TO THE MIDDLEWARE PIPELINE
3. ENDPOINT FILTERS WRAP THAT DELEGATE, ONE MORE LAYER IN
4. MapGroup PRODUCES A ROUTE GROUP THAT PREFIXES AND SHARES CONFIGURATION

Common Confusion

"Minimal APIs are a lightweight, separate mini-framework"

They're not. They run on the exact same Kestrel server, the same middleware pipeline, and the same DI container as Controller-based apps (lesson 253). "Minimal" describes the amount of ceremony required to define a handler — not a smaller or different underlying framework. You can freely mix Minimal API endpoints and Controllers in the same app.

"Minimal APIs have no way to do cross-cutting logic per endpoint" — false

This was a fair criticism early on, before IEndpointFilter existed. It's not true anymore — endpoint filters cover much of the same ground classic MVC filters do (logging, validation, short-circuiting, transforming a result), just designed around delegates and groups instead of classes and attributes.

Common Mistakes

Mistake 1 — Cramming all the business logic straight into the lambda

A 60-line MapPost delegate that validates, calls three services, and builds a response — all inline. It compiles fine, but it's untestable in isolation and impossible to skim.

Keep the delegate thin — bind parameters, call a service method, translate the result to a Results.* response. Put the actual logic in an injected service, exactly as you would for a Controller action.

Mistake 2 — Assuming a complex parameter's binding source without checking

Adding a class-typed parameter to a Minimal API handler and assuming it behaves identically to how the same-shaped parameter would behave in a Controller action. The two styles have their own, separate binding-inference rules — lesson 258 covers this carefully.

When a signature is ambiguous, use an explicit [FromQuery]/[FromBody]/[FromServices] attribute rather than relying on inference you're not certain about.

Mistake 3 — Repeating the same prefix and filters on every route by hand

Writing app.MapGet("/api/orders/..."), app.MapPost("/api/orders/..."), and so on, each separately calling .RequireAuthorization() and .AddEndpointFilter<...>().

MapGroup("/api/orders") centralizes the prefix and any shared configuration in one place — change it once, and every route in the group picks it up.

When Should I Use It?

Minimal APIs fit well when

Consider Controllers instead when

Lesson 257 makes this comparison in full, once you've formally seen both sides.

Mental Model

A Minimal API endpoint = a route + a function, where the function's parameter list is the binding contract

Remember:
· Route-matching name → from the route. Simple, unmatched type → usually the query string. Complex type → usually the body.
· A parameter typed as a registered service → resolved by DI, no constructor required.
· AddEndpointFilter = the Minimal API answer to MVC filters.
· MapGroup = a shared prefix and shared configuration for a whole family of routes.

Key Takeaway


Check Your Understanding

You've now formally learned the style you were already using in the lesson 161/162 project. Let's test whether the mechanics are clear.

1. In app.MapGet("/orders/{id:int}", (int id, IOrderService orderService) => ...), how does orderService get its value?

Show answer

Correct: B

Why B is correct: Minimal API handlers can declare a service-typed parameter directly, and the DI container supplies it at invocation time — the same resolution mechanism as constructor injection, just without a constructor because there's no class involved.

Why A is incorrect: Only id matches a route template segment; orderService has no corresponding route placeholder.

Why C is incorrect: That would defeat the entire point of DI, and Minimal APIs don't require it — the container handles construction.

Why D is incorrect: Query string binding applies to simple types like strings and numbers, not to registered service types.

Reinforcement: This is the exact behavior the lesson 161/162 project already relied on, now named explicitly.

2. What does app.MapGroup("/api/orders") primarily give you?

Show answer

Correct: B

Why B is correct: MapGroup produces a group you can map further routes onto; the prefix and any configuration (like .RequireAuthorization() or .AddEndpointFilter<T>()) chained onto the group apply to every route registered through it.

Why A is incorrect: Grouping is purely about routing and shared configuration — it has no effect on threading.

Why C is incorrect: OpenAPI documentation is a separate, unrelated concern from lesson 133 — grouping alone doesn't produce it.

Why D is incorrect: A route group is not a Controller equivalent — it has no relationship to [ApiController]'s specific behaviors (covered in lesson 257).

Reinforcement: Grouping removes repetition, not architecture — the underlying endpoints are still individual Minimal API delegates.

3. Which cross-cutting mechanism is designed specifically for Minimal API endpoints, playing a role similar to classic MVC action filters?

Show answer

Correct: B

Why B is correct: IEndpointFilter, applied via AddEndpointFilter, is the mechanism purpose-built for wrapping cross-cutting logic around Minimal API endpoint invocation.

Why A is incorrect: ActionFilterAttribute is part of the classic MVC filter system, which only applies to Controller actions, not Minimal API delegates.

Why C is incorrect: IHttpModule is a classic ASP.NET (System.Web) concept, not part of ASP.NET Core at all.

Why D is incorrect: ControllerBase is the base class for Controllers — irrelevant to the delegate-based Minimal API model.

Reinforcement: Same shape, different mechanism — IEndpointFilter is the Minimal-API-native equivalent of MVC filters.

4. A team is building a large, long-lived API with dozens of resources and heavy reliance on shared classic MVC filters across many actions. Based on this lesson's honest guidance, what's the reasonable recommendation?

Show answer

Correct: B

Why B is correct: The lesson is explicit that this is a genuine tradeoff, not a universal winner — a large API relying heavily on classic filters is exactly the scenario where Controllers' extra structure and filter ecosystem pay off.

Why A is incorrect: This directly contradicts the lesson's honest, balanced guidance — Minimal APIs are not presented as universally superior.

Why C is incorrect: Both Controllers and Minimal APIs are fully capable of building large APIs; this option invents a false limitation.

Why D is incorrect: DI works identically for Minimal APIs at any scale — that's not the deciding factor here.

Reinforcement: Choosing between the two styles is a real design decision based on team conventions, filter needs, and API size — not a fixed ranking.

You now have the formal vocabulary and mechanics behind the Minimal API code you already wrote back in lessons 161 and 162.


dotnetmadeeasy.com — Learn C# and .NET, the right way.