Minimal APIs in 2025: What They Are and When to Reach for Them

A Lighter Way to Build APIs

Minimal APIs arrived in .NET 6 as a streamlined alternative to MVC controllers. Instead of classes, attributes, and convention-based routing, you define endpoints directly in Program.cs using lambda expressions. This approach reduces ceremony and makes simple APIs faster to write and easier to understand.

The goal wasn't to replace controllers but to offer a better fit for microservices, simple CRUD APIs, and scenarios where the full MVC framework feels heavy. Minimal APIs give you the essentials (routing, model binding, dependency injection) without forcing you into a rigid structure.

You'll learn what Minimal APIs are, how they differ from controllers, and which scenarios benefit most from this approach. By the end, you'll know when to reach for Minimal APIs and when controllers still make more sense.

What Minimal APIs Look Like

A Minimal API defines endpoints using MapGet, MapPost, MapPut, and MapDelete extension methods on the WebApplication builder. Each endpoint specifies a route pattern and a delegate (often a lambda) that handles the request. There's no controller class, no action methods, and no attribute routing.

The framework handles parameter binding automatically. If your delegate needs a parameter from the route, query string, or request body, you just declare it in the method signature. Dependency injection works the same way by declaring services as parameters.

Here's a complete API with four CRUD endpoints in about 30 lines of code.

Program.cs
var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();

var products = new List<Product>
{
    new(1, "Widget", 29.99m),
    new(2, "Gadget", 39.99m)
};

app.MapGet("/products", () => products);

app.MapGet("/products/{id}", (int id) =>
    products.FirstOrDefault(p => p.Id == id) is Product product
        ? Results.Ok(product)
        : Results.NotFound());

app.MapPost("/products", (Product product) =>
{
    products.Add(product);
    return Results.Created($"/products/{product.Id}", product);
});

app.MapDelete("/products/{id}", (int id) =>
{
    var product = products.FirstOrDefault(p => p.Id == id);
    if (product is null) return Results.NotFound();

    products.Remove(product);
    return Results.NoContent();
});

app.Run();

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

Each endpoint is self-contained. The GET endpoint at /products returns the entire list. The GET by ID uses pattern matching to return either the product or a 404. POST and DELETE follow similar patterns. The Product record defines your data model inline.

Dependency Injection in Minimal APIs

Minimal APIs support full dependency injection just like controllers. You register services in the builder's service collection, then declare them as parameters in your endpoint delegates. The framework resolves and injects them automatically when requests arrive.

This works for any service registered in DI, including custom repositories, database contexts, logging, configuration, and HTTP clients. The same lifetime scopes (singleton, scoped, transient) apply. You can mix route parameters, query parameters, and injected services in the same endpoint signature.

Here's an API that uses a repository service to fetch data from a database.

Program.cs
using Microsoft.EntityFrameworkCore;

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddDbContext<AppDbContext>(options =>
    options.UseSqlite("Data Source=products.db"));

builder.Services.AddScoped<IProductRepository, ProductRepository>();

var app = builder.Build();

app.MapGet("/products", async (IProductRepository repo) =>
{
    var products = await repo.GetAllAsync();
    return Results.Ok(products);
});

app.MapGet("/products/{id}", async (int id, IProductRepository repo) =>
{
    var product = await repo.GetByIdAsync(id);
    return product is not null ? Results.Ok(product) : Results.NotFound();
});

app.MapPost("/products", async (Product product, IProductRepository repo) =>
{
    await repo.AddAsync(product);
    return Results.Created($"/products/{product.Id}", product);
});

app.Run();

public interface IProductRepository
{
    Task<List<Product>> GetAllAsync();
    Task<Product?> GetByIdAsync(int id);
    Task AddAsync(Product product);
}

public class ProductRepository : IProductRepository
{
    private readonly AppDbContext _context;
    public ProductRepository(AppDbContext context) => _context = context;

    public async Task<List<Product>> GetAllAsync() =>
        await _context.Products.ToListAsync();

    public async Task<Product?> GetByIdAsync(int id) =>
        await _context.Products.FindAsync(id);

    public async Task AddAsync(Product product)
    {
        _context.Products.Add(product);
        await _context.SaveChangesAsync();
    }
}

public class AppDbContext : DbContext
{
    public AppDbContext(DbContextOptions<AppDbContext> options) : base(options) { }
    public DbSet<Product> Products => Set<Product>();
}

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

The framework injects IProductRepository into each endpoint automatically. You don't need to manually instantiate repositories or manage their lifetimes. This keeps your endpoint logic focused on handling requests while the DI container manages dependencies.

Endpoint Filters for Cross-Cutting Concerns

Minimal APIs support endpoint filters, which are similar to action filters in MVC. Filters let you add logging, validation, exception handling, or custom authorization logic before or after endpoint execution. You can apply filters globally or to specific endpoints.

Filters run in a pipeline around your endpoint delegate. They receive the request context and can short-circuit the request, modify parameters, or wrap the response. This pattern keeps cross-cutting concerns separate from your business logic.

Here's a validation filter that checks model state before executing the endpoint.

Program.cs
using System.ComponentModel.DataAnnotations;

var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();

app.MapPost("/products", (ProductRequest request) =>
{
    return Results.Created($"/products/{Guid.NewGuid()}", request);
})
.AddEndpointFilter(async (context, next) =>
{
    var product = context.Arguments.OfType<ProductRequest>().FirstOrDefault();

    if (product is null)
        return Results.BadRequest("Product is required");

    var validationResults = new List<ValidationResult>();
    var validationContext = new ValidationContext(product);

    if (!Validator.TryValidateObject(product, validationContext,
        validationResults, true))
    {
        var errors = validationResults.Select(v => v.ErrorMessage);
        return Results.BadRequest(new { Errors = errors });
    }

    return await next(context);
});

app.Run();

public record ProductRequest(
    [Required] string Name,
    [Range(0.01, 10000)] decimal Price,
    [MinLength(3)] string Category);

The filter runs before the endpoint delegate. It extracts the ProductRequest from the arguments, validates it using data annotations, and returns a 400 Bad Request if validation fails. Only valid requests proceed to the endpoint handler. This pattern works for any cross-cutting concern you need to apply consistently.

When to Choose Minimal APIs

Minimal APIs shine in scenarios where simplicity and low ceremony matter most. Microservices with a handful of endpoints don't need the full MVC framework. Simple CRUD APIs that expose database entities benefit from minimal's directness. Backend-for-frontend services that aggregate data from other APIs stay lightweight with this approach.

You'll also find Minimal APIs work well for serverless functions, where cold start time matters. The smaller surface area and reduced reflection usage mean faster startup. Prototypes and proof-of-concept projects start faster without controller scaffolding.

Stick with controllers when you have dozens of endpoints with shared behaviors, complex authorization rules, or heavy use of action filters. Large teams benefit from MVC's conventions and structured approach. If you're building an API that will grow to 50+ endpoints with intricate business logic, controllers provide better organization.

Consider mixing both approaches. Use Minimal APIs for simple health checks, metrics endpoints, and webhooks. Use controllers for your core business logic. .NET lets you combine them in the same project without conflict.

Build Your First Minimal API

Create a minimal API from scratch that returns a list of items and supports filtering by a query parameter. You'll see how little code you need for a working HTTP API.

Steps

  1. dotnet new web -n MinimalDemo
  2. cd MinimalDemo
  3. Open Program.cs and replace with the code below
  4. dotnet run
  5. Test: curl http://localhost:5000/items
  6. Filter: curl http://localhost:5000/items?category=tools
MinimalDemo.csproj
<Project Sdk="Microsoft.NET.Sdk.Web">
  <PropertyGroup>
    <TargetFramework>net8.0</TargetFramework>
    <Nullable>enable</Nullable>
    <ImplicitUsings>enable</ImplicitUsings>
  </PropertyGroup>
</Project>
Program.cs
var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();

var items = new List<Item>
{
    new(1, "Hammer", "tools", 24.99m),
    new(2, "Screwdriver", "tools", 12.99m),
    new(3, "Paint", "supplies", 18.99m),
    new(4, "Brush", "supplies", 8.99m)
};

app.MapGet("/items", (string? category) =>
{
    if (category is null)
        return Results.Ok(items);

    var filtered = items.Where(i =>
        i.Category.Equals(category, StringComparison.OrdinalIgnoreCase));

    return Results.Ok(filtered);
});

app.Run();

record Item(int Id, string Name, string Category, decimal Price);

Run result

Without a category parameter, you'll see all four items in JSON format. Add ?category=tools to see only the hammer and screwdriver. The framework binds the query parameter automatically and handles null values gracefully.

Choosing the Right Approach

Choose Minimal APIs when you value simplicity and your endpoint count stays low. If you're building a microservice with 5-15 endpoints, minimal's lightweight approach keeps your codebase clean. The reduced boilerplate means less code to maintain and fewer abstractions to learn.

Choose MVC controllers when you need strong conventions and your API will grow large. Controllers give you action filters, model validation, and convention-based routing out of the box. If your team already knows MVC well, sticking with controllers reduces the learning curve for new features.

If unsure, start with Minimal APIs and refactor to controllers later if complexity grows. The migration path is straightforward since both share the same underlying routing and middleware infrastructure. Monitor your endpoint count and filter complexity as signals for when to switch.

Reader Questions

Are Minimal APIs replacing MVC controllers?

No, both approaches remain supported. Minimal APIs work best for lightweight services, microservices, and simple CRUD endpoints. MVC controllers still make sense for complex applications with many shared behaviors, filters, and convention-based routing. Pick based on your project's complexity.

Can I mix Minimal APIs and controllers in one project?

Yes, you can use both in the same application. Call both AddControllers and map minimal endpoints. This works well when migrating gradually or when different parts of your API have different complexity levels. Just keep routing patterns consistent to avoid confusion.

Do Minimal APIs support OpenAPI and Swagger?

Yes, .NET 8 includes built-in OpenAPI support for Minimal APIs. Use the Microsoft.AspNetCore.OpenApi package and call WithOpenApi on endpoints. Swashbuckle also works with minimal endpoints. The generated documentation includes parameters, request bodies, and response types automatically.

What about authentication and authorization?

Minimal APIs fully support authentication and authorization. Use RequireAuthorization on endpoints or create authorization policies. The same middleware, JWT validation, and cookie authentication that work with controllers also work with minimal endpoints. Security capabilities are identical.

Back to Articles