
Learn how to build lightweight Minimal APIs in .NET 8 with C#. Step-by-step tutorial, code examples, and best practices. Start building fast APIs today!
If you want to build lightweight APIs fast, Minimal APIs in .NET 8 are the fastest way to ship a production-ready HTTP endpoint with the least amount of boilerplate. Introduced in .NET 6 and dramatically improved through .NET 8, Minimal APIs let you create a fully functional REST API in just a handful of lines of C# code—no controllers, no attributes, and no ceremony. In this tutorial, you'll learn exactly how to create a Minimal API in .NET 8, wire up dependency injection, return JSON, validate input, and follow the best practices that separate hobby projects from production-grade services.
Whether you're a beginner searching for a Minimal API tutorial, an intermediate developer looking for best practices, or a senior engineer evaluating performance trade-offs, this guide covers it all with runnable examples.
What Are Minimal APIs in .NET 8?
Minimal APIs are a streamlined approach to building HTTP APIs in ASP.NET Core. Instead of the traditional MVC pattern with controllers, base classes, and routing attributes, you define endpoints directly on the WebApplication instance. The result is less code, faster startup, and a smaller memory footprint—ideal for microservices, serverless functions, and high-throughput services.
Why use Minimal APIs? The key benefits are:
- Less boilerplate — A working endpoint takes 4–5 lines, not a whole class.
- Better performance — Reduced overhead compared to the full MVC pipeline.
- Faster onboarding — New developers read the entire API in one file.
- Cloud-native fit — Perfect for containers and microservices where startup time matters.
How to Create a Minimal API in .NET 8
Let's start with the classic "Hello World". Make sure you have the .NET 8 SDK installed, then create a new project:
dotnet new web -n MinimalApiDemo
cd MinimalApiDemo
dotnet run
Open Program.cs and you'll see the entire application—there's no Startup.cs file thanks to top-level statements. Here's the minimal setup:
var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();
app.MapGet("/", () => "Hello from Minimal APIs in .NET 8!");
app.Run();
That's a complete, running web server. The MapGet method maps an HTTP GET request to a handler. The handler is just a lambda—.NET handles serialization, routing, and the response automatically.
Building a REST API in C# with CRUD Endpoints
A real API needs more than a greeting. Let's build a small Todo service that demonstrates all the HTTP verbs. This is the core of building a REST API in C# with Minimal APIs.
var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();
var todos = new List<Todo>();
// GET all todos
app.MapGet("/todos", () => todos);
// GET a single todo by id
app.MapGet("/todos/{id}", (int id) =>
{
var todo = todos.FirstOrDefault(t => t.Id == id);
return todo is not null ? Results.Ok(todo) : Results.NotFound();
});
// POST a new todo
app.MapPost("/todos", (Todo todo) =>
{
todos.Add(todo);
return Results.Created($"/todos/{todo.Id}", todo);
});
// PUT to update an existing todo
app.MapPut("/todos/{id}", (int id, Todo input) =>
{
var todo = todos.FirstOrDefault(t => t.Id == id);
if (todo is null) return Results.NotFound();
todo.Title = input.Title;
todo.IsComplete = input.IsComplete;
return Results.NoContent();
});
// DELETE a todo
app.MapDelete("/todos/{id}", (int id) =>
{
var todo = todos.FirstOrDefault(t => t.Id == id);
if (todo is null) return Results.NotFound();
todos.Remove(todo);
return Results.NoContent();
});
app.Run();
record Todo
{
public int Id { get; set; }
public string? Title { get; set; }
public bool IsComplete { get; set; }
}
Notice how the Results class gives you typed HTTP responses—Results.Ok(), Results.NotFound(), Results.Created(), and Results.NoContent()—so you control status codes precisely without extra plumbing. .NET 8 also introduced TypedResults, which is strongly typed and improves testability and OpenAPI documentation:
app.MapGet("/todos/{id}", Results<Ok<Todo>, NotFound> (int id) =>
{
var todo = todos.FirstOrDefault(t => t.Id == id);
return todo is not null
? TypedResults.Ok(todo)
: TypedResults.NotFound();
});
Dependency Injection in Minimal APIs
One reason Minimal APIs scale to real projects is first-class dependency injection. You register services on the builder and inject them directly into endpoint handlers as parameters—ASP.NET Core resolves them automatically.
var builder = WebApplication.CreateBuilder(args);
// Register a service
builder.Services.AddSingleton<ITodoService, TodoService>();
var app = builder.Build();
// Inject ITodoService into the handler
app.MapGet("/todos", (ITodoService service) => service.GetAll());
app.Run();
interface ITodoService
{
IEnumerable<Todo> GetAll();
}
class TodoService : ITodoService
{
private readonly List<Todo> _todos = new();
public IEnumerable<Todo> GetAll() => _todos;
}
Why this matters: The framework distinguishes between route parameters, query strings, the request body, and injected services based on their type and source. This keeps your handlers clean and fully testable without coupling them to HTTP details.
Organizing Endpoints with Route Groups
A common pitfall with Minimal APIs is dumping every endpoint into one giant Program.cs. .NET 8 solves this with route groups via MapGroup, letting you apply a shared prefix, filters, and metadata to related endpoints.
var todosApi = app.MapGroup("/todos")
.WithTags("Todos");
todosApi.MapGet("/", (ITodoService service) => service.GetAll());
todosApi.MapGet("/{id}", (int id, ITodoService service) => service.GetById(id));
todosApi.MapPost("/", (Todo todo, ITodoService service) => service.Add(todo));
For larger projects, move groups into extension methods (e.g. app.MapTodoEndpoints()) in separate files. This keeps each feature self-contained and maintains readability as your API grows.
Validation and Error Handling Best Practices
Minimal APIs don't include automatic model validation out of the box like MVC controllers do, so this is a frequent source of bugs. The cleanest approach in .NET 8 is to use endpoint filters, which run before your handler executes.
app.MapPost("/todos", (Todo todo) =>
{
todos.Add(todo);
return Results.Created($"/todos/{todo.Id}", todo);
})
.AddEndpointFilter(async (context, next) =>
{
var todo = context.GetArgument<Todo>(0);
if (string.IsNullOrWhiteSpace(todo.Title))
{
return Results.ValidationProblem(new Dictionary<string, string[]>
{
{ "Title", new[] { "Title is required." } }
});
}
return await next(context);
});
Endpoint filters are reusable and composable, making them ideal for cross-cutting concerns like validation, logging, and authentication checks.
Adding OpenAPI and Swagger Documentation
Documentation is essential for any public-facing API. With .NET 8, enabling Swagger UI takes just a few lines, and route groups plus TypedResults produce rich, accurate schemas automatically.
builder.Services.AddEndpointsApiExplorer();
builder.Services.AddSwaggerGen();
var app = builder.Build();
if (app.Environment.IsDevelopment())
{
app.UseSwagger();
app.UseSwaggerUI();
}
Minimal APIs vs Controllers: When to Use Each
A question that comes up constantly: should you use Minimal APIs or traditional controllers? Here's the practical guidance:
- Choose Minimal APIs for microservices, serverless endpoints, lightweight back-ends, prototypes, and any service where startup time and simplicity are priorities.
- Choose controllers for large enterprise applications with complex conventions, heavy use of model binding, action filters, and teams already standardized on MVC patterns.
The good news: both run on the same ASP.NET Core runtime, deliver comparable performance for most workloads, and can coexist in the same project. You're not locked in.
Common Pitfalls to Avoid
- Putting all logic in Program.cs — Use route groups and extension methods to stay organized.
- Forgetting validation — Minimal APIs won't validate models for you; add endpoint filters.
- Ignoring async — For database or network calls, use
asynchandlers and returnTask<IResult>to avoid blocking threads. - Returning raw exceptions — Use
app.UseExceptionHandler()and the Problem Details standard for consistent error responses. - Skipping TypedResults — They improve testability and produce better OpenAPI docs than untyped
Results.
Conclusion: Key Takeaways
Minimal APIs in .NET 8 are the modern, recommended way to build lightweight APIs fast in C#. They strip away boilerplate while retaining the full power of ASP.NET Core—dependency injection, middleware, authentication, and OpenAPI support all work seamlessly. For microservices and cloud-native applications, they're often the best default choice.
Here's what to remember:
- Define endpoints directly with
MapGet,MapPost,MapPut, andMapDelete. - Use
TypedResultsfor strongly-typed, testable, well-documented responses. - Inject services directly into handlers via dependency injection.
- Organize at scale with
MapGroupand extension methods. - Add validation with endpoint filters and handle errors with Problem Details.
Now you have everything you need to build a production-ready REST API in C# using Minimal APIs in .NET 8. Spin up a new project, copy the examples above, and ship your first lightweight API today. Happy coding!
Your go-to resource for C#, .NET, and modern software development. Follow along for daily tutorials, tips, and real-world examples.
Comments
Post a Comment