Skip to main content

ASP.Net Core: Routing - Minimal API

Updated 3 min readMarkdown

1. Basic Explicit Mapping (The Foundation)

In Minimal APIs, every route is mapped explicitly using extension methods on the WebApplication (app) object. There is no auto-discovery by default.

  • Standard HTTP Verbs: Mapping is done by calling the method corresponding to the HTTP verb.

      // GET: /api/hello
      app.MapGet("/api/hello", () => "Hello World");
    
      // POST: /api/users
      app.MapPost("/api/users", (UserDto user) => {
          // Save user logic...
          return Results.Created($"/api/users/{user.Id}", user);
      });
    
      // PUT: /api/users/1
      app.MapPut("/api/users/{id}", (int id, UserDto user) => { ... });
    
      // DELETE: /api/users/1
      app.MapDelete("/api/users/{id}", (int id) => { ... });
    
  • Handler Types: You can map a route to a lambda (inline function) or a method reference.

      // Option A: Inline Lambda (Good for simple logic)
      app.MapGet("/inline", () => "Inline");
    
      // Option B: Method Reference (Cleaner)
      string LocalFunction() => "Local Function";
      app.MapGet("/method", LocalFunction);
    

2. Route Parameters & Constraints

Just like Controllers, Minimal APIs support capturing values from the URL and enforcing data types.

  • Simple Parameters: The parameter name in the lambda must match the route segment {...}.

      // URL: /users/5
      app.MapGet("/users/{id}", (int id) => $"User ID: {id}");
    
  • Constraints (Data Type Enforcement): Use : to enforce rules. If the rule fails, the API returns 404 Not Found.

      // Matches: /products/10 (int)
      // Skips: /products/abc
      app.MapGet("/products/{id:int}", (int id) => ...);
    
      // Matches strings with min length 3
      app.MapGet("/search/{query:minlength(3)}", (string query) => ...);
    
  • Catch-All (Wildcards): Use {*name} to capture the rest of the URL path.

      // Matches: /files/images/logo.png
      // path = "images/logo.png"
      app.MapGet("/files/{*path}", (string path) => ...);
    

3. Route Groups (Essential for Organization)

Instead of repeating the same prefix /api/users for every route, use Route Groups. This also allows you to apply middleware (like Authentication) to a whole batch of endpoints at once.

// 1. Create a Group
var userGroup = app.MapGroup("/api/users");

// 2. Map routes relative to the group
// Final URL: /api/users
userGroup.MapGet("/", GetUsers);

// Final URL: /api/users/{id}
userGroup.MapGet("/{id}", GetUserById);

// 3. Apply Security to the WHOLE group
userGroup.RequireAuthorization();

4. Advanced Parameter Binding

Sometimes you need to tell ASP.NET exactly where to look for data (Body, Query, Header), rather than letting it guess.

using Microsoft.AspNetCore.Mvc; // Required for these attributes

app.MapPost("/orders/{id}", (
    [FromRoute] int id, 
    [FromBody] OrderDto order, 
    [FromQuery] int page, 
    [FromHeader(Name = "X-Request-ID")] string requestId
) => {
    return $"Order {id}, Page {page}, Request {requestId}";
});

5. Industry Standard: The "Module" Pattern (Real World)

Problem: In a real app with 500+ endpoints, Program.cs becomes massive and unreadable if you list all app.MapGet calls there.

Solution: Use the Carter library or a custom Module Interface to split routes into separate files (Feature Slices).

The Carter Library Approach (Industry Favorite)

This achieves the exact same result but removes the need for writing the EndpointExtensions.cs file manually. It is battle-tested and supports advanced features like validation pipelines.

Install: dotnet add package Carter

1. The Feature Module (/Features/Products/GetProducts/GetProductsEndpoint.cs)

Inherit from ICarterModule instead of a custom interface.

using Carter;
using Microsoft.AspNetCore.Mvc;

namespace MyApi.Features.Products.GetProducts;

public class GetProductsEndpoint : ICarterModule
{
    public void AddRoutes(IEndpointRouteBuilder app)
    {
        var group = app.MapGroup("/api/products");

        group.MapGet("/", HandleAsync)
             .WithName("GetProducts")
             .WithOpenApi();
    }

    internal async Task<IResult> HandleAsync(ILogger<GetProductsEndpoint> logger)
    {
        logger.LogInformation("Fetching products...");

        var data = new[] { "Laptop", "Mouse", "Keyboard" };
        return Results.Ok(data);
    }
}

2. The Wiring (Program.cs)

It's even shorter.

using Carter; // Import the library

var builder = WebApplication.CreateBuilder(args);

// 1. Add Carter to DI
builder.Services.AddCarter(); 
builder.Services.AddEndpointsApiExplorer();
builder.Services.AddSwaggerGen();

var app = builder.Build();

// 2. Map Carter (Auto-scans and maps all ICarterModules)
app.MapCarter(); 

if (app.Environment.IsDevelopment())
{
    app.UseSwagger();
    app.UseSwaggerUI();
}

app.Run();

No comments yet. Be the first to comment.

More from this blog