Minimal APIs vs MVC: Choosing the Right Shape for Small Services
7 min read
Intermediate
Choosing Your API Style
Picture this: you're building a weather notification service. It needs three endpoints for fetching forecasts, updating user preferences, and triggering alerts. Do you scaffold a full MVC controller structure with action methods, filters, and routing attributes, or do you map three simple functions directly to routes?
Minimal APIs let you define endpoints with less code and faster startup times, perfect for small services and microservices. MVC controllers give you structure, conventions, and rich tooling that shine when your API grows beyond a handful of routes. The decision affects your code organization, testing approach, and how quickly new developers understand your project.
You'll see concrete examples of both approaches, learn when each fits best, and walk away with a pattern you can apply to your next service. By the end, you'll have working code for both styles and a clear decision framework.
Minimal API Fundamentals
Minimal APIs strip away the controller class ceremony. You define routes and handlers directly in Program.cs using lambda expressions or local functions. This reduces boilerplate and makes small services easier to scan.
The syntax uses extension methods on WebApplication. You call app.MapGet, app.MapPost, or other HTTP verb methods, pass a route pattern, and provide a handler. The framework infers parameter binding from your handler signature, pulling values from route data, query strings, or request bodies.
Here's a basic Minimal API with three endpoints for a product catalog service. Notice how the entire API surface lives in one file without controller classes.
Program.cs
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddEndpointsApiExplorer();
builder.Services.AddSwaggerGen();
var app = builder.Build();
if (app.Environment.IsDevelopment())
{
app.UseSwagger();
app.UseSwaggerUI();
}
app.UseHttpsRedirection();
// GET all products
app.MapGet("/products", () =>
{
var products = new[]
{
new { Id = 1, Name = "Laptop", Price = 999.99m },
new { Id = 2, Name = "Mouse", Price = 24.99m },
new { Id = 3, Name = "Keyboard", Price = 79.99m }
};
return Results.Ok(products);
});
// GET product by ID
app.MapGet("/products/{id:int}", (int id) =>
{
if (id <= 0) return Results.BadRequest("Invalid product ID");
var product = new { Id = id, Name = "Sample Product", Price = 49.99m };
return Results.Ok(product);
});
// POST new product
app.MapPost("/products", (Product product) =>
{
if (string.IsNullOrEmpty(product.Name))
return Results.BadRequest("Product name required");
return Results.Created($"/products/{product.Id}", product);
});
app.Run();
record Product(int Id, string Name, decimal Price);
The framework automatically deserializes JSON payloads into your Product record. Route constraints like {'{'}id:int{'}'} ensure type safety. Results.Ok, Results.BadRequest, and Results.Created return proper HTTP status codes with bodies.
This style works beautifully when you have a small surface area. Everything is visible in one place, and there's no hunting through controller folders to understand what routes exist.
MVC Controller Pattern
MVC controllers organize endpoints into classes. Each public method becomes an action that responds to HTTP requests. You get conventions for routing, automatic model validation, and a clear separation between different resource types.
Controllers use attributes for routing and HTTP verbs. The [ApiController] attribute enables automatic model state validation and binding source inference. You return ActionResult types that provide IntelliSense and compile-time safety for status codes.
Here's the same product API using a controller. Notice the additional structure and how concerns are organized into methods.
Controllers/ProductsController.cs
using Microsoft.AspNetCore.Mvc;
namespace ProductApi.Controllers;
[ApiController]
[Route("[controller]")]
public class ProductsController : ControllerBase
{
private readonly ILogger<ProductsController> _logger;
public ProductsController(ILogger<ProductsController> logger)
{
_logger = logger;
}
[HttpGet]
public ActionResult<IEnumerable<Product>> GetAll()
{
var products = new[]
{
new Product(1, "Laptop", 999.99m),
new Product(2, "Mouse", 24.99m),
new Product(3, "Keyboard", 79.99m)
};
_logger.LogInformation("Retrieved {Count} products", products.Length);
return Ok(products);
}
[HttpGet("{id:int}")]
public ActionResult<Product> GetById(int id)
{
if (id <= 0)
{
_logger.LogWarning("Invalid product ID requested: {Id}", id);
return BadRequest("Invalid product ID");
}
var product = new Product(id, "Sample Product", 49.99m);
return Ok(product);
}
[HttpPost]
public ActionResult<Product> Create(Product product)
{
if (!ModelState.IsValid)
return BadRequest(ModelState);
_logger.LogInformation("Created product: {Name}", product.Name);
return CreatedAtAction(nameof(GetById), new { id = product.Id }, product);
}
}
public record Product(int Id, string Name, decimal Price);
Controllers make dependency injection natural. You inject services through the constructor, and the framework handles instantiation per request. Action methods have clear names, making navigation and testing straightforward.
The [ApiController] attribute automatically returns 400 responses when model validation fails. CreatedAtAction generates a Location header pointing to the new resource, following REST conventions properly.
Key Differences That Matter
Startup time favors Minimal APIs. They skip the controller discovery phase that MVC performs during application startup. For microservices that start and stop frequently in container orchestration, this saves 50-100ms on cold starts.
Code organization differs fundamentally. Minimal APIs keep everything in Program.cs, which is great for services with 5-10 endpoints. MVC spreads endpoints across controller classes, which helps when you have dozens of routes grouped by resource type. Choose based on your API size, not personal preference.
Middleware and filters work differently. MVC has action filters, result filters, and exception filters that apply to controllers or actions. Minimal APIs use endpoint filters, which are newer but less mature in tooling support. If you rely heavily on custom filters for cross-cutting concerns, MVC is still smoother.
Testing approaches vary. Controller actions are instance methods you can test by newing up the controller and calling methods. Minimal API handlers are often inline lambdas, which require integration tests or extracting handlers into testable methods. For TDD practitioners, controllers offer easier unit testing.
Choosing the Right Approach
Choose Minimal APIs when you're building a microservice with fewer than 10 endpoints, especially if they're all related to a single domain concept. The reduced ceremony means less code to maintain, and faster cold starts help in serverless or container-heavy deployments. Teams that value simplicity over conventions will appreciate the directness.
Choose MVC controllers when your API serves multiple resource types with complex relationships. If you need action filters for authentication, validation, or logging applied declaratively, MVC's attribute-based approach is cleaner. Larger teams benefit from the structure since conventions make it easier to find where functionality lives.
If you're migrating from ASP.NET Framework, stick with controllers initially. Your team already understands the patterns, and porting is more straightforward. You can introduce Minimal APIs gradually for new, isolated features without disrupting existing code.
Monitor your decision. If your Minimal API grows beyond 15-20 endpoints, consider refactoring to controllers. If your MVC API only has three endpoints and no custom filters, simplify to Minimal APIs. The pattern should serve the problem, not the other way around.
Try It Yourself
Build a small API using both approaches to see the difference firsthand. This example creates a task tracking service with Minimal APIs, then shows you how to organize it with controllers.
Steps
Create a new web API project: dotnet new web -n TaskApi
Navigate to the folder: cd TaskApi
Replace Program.cs with the code below
Run the application: dotnet run
Test with curl: curl https://localhost:5001/tasks
Program.cs
var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();
var tasks = new List<TodoTask>
{
new(1, "Write API comparison article", false),
new(2, "Review Minimal API patterns", true)
};
app.MapGet("/tasks", () => Results.Ok(tasks));
app.MapGet("/tasks/{id:int}", (int id) =>
{
var task = tasks.FirstOrDefault(t => t.Id == id);
return task is not null ? Results.Ok(task) : Results.NotFound();
});
app.MapPost("/tasks", (TodoTask task) =>
{
tasks.Add(task);
return Results.Created($"/tasks/{task.Id}", task);
});
app.MapPut("/tasks/{id:int}/complete", (int id) =>
{
var task = tasks.FirstOrDefault(t => t.Id == id);
if (task is null) return Results.NotFound();
var updated = task with { IsComplete = true };
tasks[tasks.IndexOf(task)] = updated;
return Results.Ok(updated);
});
app.Run();
record TodoTask(int Id, string Title, bool IsComplete);
When should I use Minimal APIs instead of MVC controllers?
Use Minimal APIs for small services with fewer than 10 endpoints, microservices, or simple CRUD operations. They reduce ceremony and startup time. Choose MVC when you need complex routing, action filters, model binding customization, or you're building a large API with dozens of endpoints.
Can I mix Minimal APIs and MVC controllers in the same project?
Yes, you can use both in the same application. Register both with builder.Services.AddControllers() and app.MapControllers() for MVC, plus your minimal endpoint mappings. This works well when migrating gradually or when different parts of your API have different complexity needs.
Do Minimal APIs perform better than MVC controllers?
Minimal APIs have slightly lower overhead because they skip controller instantiation and action method discovery. Benchmarks show 10-15% better throughput for simple endpoints. However, for most real-world apps, database and business logic dominate performance, making the difference negligible.
How do I handle validation in Minimal APIs?
Use endpoint filters for validation. Install FluentValidation.AspNetCore, create validators for your DTOs, then apply them with endpoint filters or the WithValidator extension. You can also manually validate in the handler and return Results.ValidationProblem for consistent error responses.
What about OpenAPI and Swagger with Minimal APIs?
Minimal APIs support OpenAPI through the same Swashbuckle package. Use .WithName(), .WithTags(), .Produces(), and .WithOpenApi() extension methods to annotate endpoints. .NET 8 improved type inference, so most metadata is automatic, but you may need manual annotations for complex scenarios.