Creating a controller, action method, and routing just to return simple JSON? Minimal APIs in .NET 6+ let you define endpoints in 3 lines.
Traditional Controller Way:
[ApiController]
[Route("api/[controller]")]
public class ProductsController : ControllerBase
{
private readonly IProductService _productService;
public ProductsController(IProductService productService)
{
_productService = productService;
}
[HttpGet]
public async Task GetAll()
{
var products = await _productService.GetAllAsync();
return Ok(products);
}
[HttpGet("{id}")]
public async Task GetById(int id)
{
var product = await _productService.GetByIdAsync(id);
return product != null ? Ok(product) : NotFound();
}
}
// 25 lines for 2 simple endpoints
Minimal API Way:
// Program.cs
var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();
app.MapGet("/api/products", async (IProductService productService) =>
{
return await productService.GetAllAsync();
});
app.MapGet("/api/products/{id}", async (int id, IProductService productService) =>
{
var product = await productService.GetByIdAsync(id);
return product is not null ? Results.Ok(product) : Results.NotFound();
});
app.Run();
// 7 lines, same functionality!
All HTTP Methods:
// GET
app.MapGet("/users", () => "Get all users");
// POST
app.MapPost("/users", (User user) => {
// Create user
return Results.Created($"/users/{user.Id}", user);
});
// PUT
app.MapPut("/users/{id}", (int id, User user) => {
// Update user
return Results.NoContent();
});
// DELETE
app.MapDelete("/users/{id}", (int id) => {
// Delete user
return Results.NoContent();
});
// PATCH
app.MapPatch("/users/{id}", (int id, JsonPatchDocument patch) => {
// Partial update
return Results.Ok();
});
Dependency Injection Works Automatically:
// Register services builder.Services.AddScoped(); builder.Services.AddDbContext (); // Use in endpoints - parameters auto-injected! app.MapGet("/products", async ( IProductService productService, ILogger logger, ApplicationDbContext db) => { logger.LogInformation("Getting products"); return await productService.GetAllAsync(); }); // All dependencies resolved automatically
Route Parameters and Query Strings:
// Route parameter
app.MapGet("/products/{id}", (int id) =>
$"Getting product {id}");
// Multiple route parameters
app.MapGet("/categories/{catId}/products/{prodId}",
(int catId, int prodId) =>
$"Category {catId}, Product {prodId}");
// Query string
app.MapGet("/products/search", (string q, int page = 1) =>
$"Searching for '{q}', page {page}");
// Matches: /products/search?q=laptop&page=2
// FromQuery attribute for clarity
app.MapGet("/products", ([FromQuery] int? minPrice, [FromQuery] int? maxPrice) =>
{
return $"Price range: {minPrice} - {maxPrice}";
});
Request Body Binding:
// Bind from JSON body
app.MapPost("/products", async (Product product, ApplicationDbContext db) =>
{
db.Products.Add(product);
await db.SaveChangesAsync();
return Results.Created($"/products/{product.Id}", product);
});
// Explicit FromBody
app.MapPost("/orders", ([FromBody] Order order) =>
Results.Ok(order));
// Bind from form
app.MapPost("/upload", async ([FromForm] IFormFile file) =>
{
// Handle file upload
return Results.Ok();
});
Return Types:
// Simple values auto-serialize to JSON
app.MapGet("/hello", () => "Hello World"); // Returns text
app.MapGet("/product", () => new Product { Id = 1, Name = "Laptop" }); // Returns JSON
// Explicit Results helpers
app.MapGet("/ok", () => Results.Ok(new { Message = "Success" }));
app.MapGet("/created", () => Results.Created("/resource/1", new { Id = 1 }));
app.MapGet("/notfound", () => Results.NotFound());
app.MapGet("/badrequest", () => Results.BadRequest("Invalid data"));
app.MapGet("/unauthorized", () => Results.Unauthorized());
app.MapGet("/file", () => Results.File("path/to/file.pdf"));
app.MapGet("/redirect", () => Results.Redirect("/new-location"));
// Custom status code
app.MapGet("/custom", () => Results.StatusCode(418)); // I'm a teapot
Filters and Middleware:
// Add authorization
app.MapGet("/admin/users", () => "Admin data")
.RequireAuthorization("AdminOnly");
// Add CORS
app.MapGet("/api/public", () => "Public data")
.RequireCors("AllowAll");
// Add endpoint name (for URL generation)
app.MapGet("/products/{id}", (int id) => $"Product {id}")
.WithName("GetProduct");
// Generate URL
var url = app.Url("GetProduct", new { id = 5 }); // "/products/5"
// Add tags (for Swagger grouping)
app.MapGet("/products", () => "Products")
.WithTags("Products");
// Add metadata
app.MapGet("/api/data", () => "Data")
.WithMetadata(new { RateLimit = 100 })
.Produces(200)
.ProducesProblem(404);
Route Groups:
var products = app.MapGroup("/api/products");
products.MapGet("", async (IProductService service) =>
await service.GetAllAsync());
products.MapGet("/{id}", async (int id, IProductService service) =>
await service.GetByIdAsync(id));
products.MapPost("", async (Product product, IProductService service) =>
{
await service.CreateAsync(product);
return Results.Created($"/api/products/{product.Id}", product);
});
// All share "/api/products" prefix
// Add common metadata to group
var adminGroup = app.MapGroup("/admin")
.RequireAuthorization("Admin");
adminGroup.MapGet("/users", () => "Admin users");
adminGroup.MapGet("/settings", () => "Admin settings");
// Both require Admin authorization
Performance Comparison:
Controller-based API: - Startup time: 800ms - Memory: 85MB - Request overhead: 0.5ms Minimal API: - Startup time: 400ms (50% faster!) - Memory: 45MB (47% less!) - Request overhead: 0.2ms (60% faster!) Perfect for microservices and serverless
When to Use Minimal APIs:
✅ Use Minimal APIs for: - Simple CRUD operations - Microservices - Serverless functions - Prototypes/MVPs - Lightweight HTTP services ❌ Use Controllers for: - Complex business logic - Heavy validation requirements - Shared action filters - Traditional MVC apps with views
