Imagine you are building a backend for an online learning platform. Your frontend needs endpoints to retrieve courses, register users, track lesson progress, and manage portfolios. You want to expose these features through HTTP without writing unnecessary boilerplate for every operation.
Minimal APIs are an approach to building HTTP APIs in ASP.NET Core with concise route definitions and request handlers. Instead of creating a controller class and decorating every action with attributes, you can map HTTP methods and URL patterns directly in your application.
Minimal APIs support dependency injection, route and query parameters, request-body binding, authentication, authorization, middleware, OpenAPI integration, and asynchronous operations. They are suitable for lightweight APIs, microservices, prototypes, and applications where direct endpoint definitions provide a clear structure.
However, minimal does not mean that your application must contain all its business logic inside Program.cs. As a project grows, you can organize endpoint mappings into separate files, group related routes, and delegate business operations to application services.
1. What Are Minimal APIs?
Minimal APIs provide a lightweight way to define HTTP endpoints in ASP.NET Core. An endpoint specifies the route, HTTP method, and handler that processes the incoming request.
For example, the following code creates a simple endpoint:
var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();
app.MapGet("/", () => "Welcome to the API");
app.Run();
When a client sends a GET request to the root URL, the handler returns a text response. The application uses WebApplication.CreateBuilder() to configure the host and its services, and Build() creates the application that will process incoming requests.
The MapGet() method maps an HTTP GET request to a route handler. Similar methods exist for POST, PUT, PATCH, and DELETE operations.
Minimal APIs have been available since ASP.NET Core 6. They have continued to gain capabilities across subsequent .NET releases, so the available features depend on the framework version used by your project.
2. Creating a Minimal API Project
You can create a new ASP.NET Core Web API project using the .NET CLI. Open a terminal and execute:
dotnet new web -n MyMinimalApi
cd MyMinimalApi
dotnet run
The web template creates a minimal ASP.NET Core web application. You can then define endpoints in Program.cs or move them into dedicated endpoint-registration classes as the application becomes larger.
If you prefer a template configured specifically for HTTP API development, you can also use:
dotnet new webapi -n MyApi
Template defaults can differ between SDK versions. Some versions generate a minimal API project by default, while others may offer controller-based options. Choose the template and configuration that fit your application.
For a broader introduction to the available .NET application types, read .NET Application Architectures.
3. Mapping HTTP Methods and Routes
A REST API commonly exposes different operations through HTTP methods. Minimal APIs provide dedicated mapping methods for these operations.
MapGet(): Handles GET requests, commonly used to retrieve resources.MapPost(): Handles POST requests, commonly used to create resources or initiate operations.MapPut(): Handles PUT requests, commonly used to replace a resource or its representation.MapPatch(): Handles PATCH requests, commonly used for partial updates when an appropriate patch contract is defined.MapDelete(): Handles DELETE requests, commonly used to delete a resource.
Consider a course-management API:
app.MapGet("/api/courses", () =>
{
return Results.Ok("Return all courses");
});
app.MapGet("/api/courses/{id:int}", (int id) =>
{
return Results.Ok(new { Id = id });
});
app.MapPost("/api/courses", () =>
{
return Results.Created(
"/api/courses/1",
new { Id = 1, Title = "C# Fundamentals" });
});
app.MapPut("/api/courses/{id:int}", (int id) =>
{
return Results.NoContent();
});
app.MapDelete("/api/courses/{id:int}", (int id) =>
{
return Results.NoContent();
});
These examples demonstrate endpoint mapping and response types, not a complete persistence implementation. A real API would validate request data, execute the appropriate application operation, and return results based on what actually happened.
Use consistent resource-oriented route names and appropriate HTTP status codes. Learn more in REST API Design Best Practices in .NET.
4. Route Parameters, Query Parameters, and Request Bodies
An endpoint often needs information from the URL, query string, or HTTP request body. Minimal APIs support binding these values to handler parameters.
Route parameters
Route parameters identify a value embedded in the URL. They are useful when retrieving or modifying a specific resource.
app.MapGet("/api/courses/{id:int}", (int id) =>
{
return Results.Ok(new
{
CourseId = id
});
});
The {id:int} route constraint matches an integer-shaped route segment. A request to /api/courses/25 supplies the value 25 to the handler. A segment that does not match the constraint will not match this route.
Query parameters
Query parameters are useful for filtering, searching, sorting, and pagination. For example, a course endpoint may accept a search term and a page size:
app.MapGet("/api/courses/search",
(string? search, int? pageSize) =>
{
return Results.Ok(new
{
SearchTerm = search,
PageSize = pageSize ?? 20
});
});
A request such as /api/courses/search?search=dotnet&pageSize=10 supplies these values to the handler. The example applies a default when pageSize is omitted, but a production API should also validate acceptable ranges and enforce a maximum page size.
Request body
When a client sends structured data, Minimal APIs can bind a JSON request body to a compatible C# type.
public sealed record CreateCourseRequest(
string Title,
decimal Price);
app.MapPost(
"/api/courses",
(CreateCourseRequest request) =>
{
return Results.Ok(new
{
request.Title,
request.Price
});
});
The handler receives the deserialized request object. In a real application, validate the title and price, invoke the relevant application service, persist the new course, and return a response that accurately reflects the operation's outcome.
5. Returning Appropriate HTTP Responses
Minimal APIs provide the Results and TypedResults APIs for creating HTTP responses. Choosing an accurate response helps clients understand whether an operation succeeded, failed, or requires additional action.
Common response patterns include:
Results.Ok(value)returns HTTP 200 with a response body.Results.Created(uri, value)returns HTTP 201 for a successfully created resource and identifies its location.Results.NoContent()returns HTTP 204 when a successful operation has no response body.Results.BadRequest()returns HTTP 400 for a request that cannot be accepted.Results.NotFound()returns HTTP 404 when the requested resource does not exist.Results.Problem()produces a Problem Details-style error response.
For example, a product endpoint can return 404 when the product cannot be found instead of returning HTTP 200 with a misleading success response.
app.MapGet(
"/api/products/{id:int}",
async (int id, IProductService service,
CancellationToken cancellationToken) =>
{
var product = await service.GetByIdAsync(
id,
cancellationToken);
return product is null
? Results.NotFound()
: Results.Ok(product);
});
In this example, IProductService is resolved through dependency injection, and the result determines the HTTP status code. The service must be implemented and registered in the application.
6. Using Typed Results
While Results makes it convenient to return different HTTP responses, TypedResults provides concrete result types that can improve compile-time information and endpoint metadata.
A handler that can return either a product or a not-found response can use Results<T1, T2> to express both possibilities.
using Microsoft.AspNetCore.Http.HttpResults;
app.MapGet(
"/api/products/{id:int}",
async Task<Results<Ok<ProductDto>, NotFound>> (
int id,
IProductService service,
CancellationToken cancellationToken) =>
{
var product = await service.GetByIdAsync(
id,
cancellationToken);
if (product is null)
{
return TypedResults.NotFound();
}
return TypedResults.Ok(product);
});
The example assumes that the service returns a ProductDto or null. The explicit result union makes the expected success and missing-resource responses visible in the handler's return type.
Typed results can also improve endpoint metadata used by API documentation. Use them when explicit response types help readability and tooling. They are not mandatory for every endpoint; ordinary result APIs remain useful for simpler scenarios.
7. Using Dependency Injection in Minimal APIs
Minimal APIs are integrated with ASP.NET Core's dependency-injection system. When a handler declares a registered service parameter, ASP.NET Core can resolve the service and provide it when invoking the endpoint.
This lets endpoints delegate database access, business operations, email delivery, and other work to application services rather than implementing all behavior inline.
public interface ICourseService
{
Task<IReadOnlyList<CourseDto>> GetAllAsync(
CancellationToken cancellationToken);
}
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddScoped<ICourseService, CourseService>();
var app = builder.Build();
app.MapGet(
"/api/courses",
async (
ICourseService service,
CancellationToken cancellationToken) =>
{
var courses = await service.GetAllAsync(
cancellationToken);
return Results.Ok(courses);
});
app.Run();
The code demonstrates the registration and endpoint pattern. CourseService and CourseDto must exist, and the service may depend on a repository or database context that also needs to be registered.
Prefer the appropriate lifetime for every dependency. A scoped service is a common choice for operations that depend on an EF Core DbContext. Avoid keeping request-scoped dependencies in long-lived singleton objects.
Learn more in Dependency Injection in .NET.
8. Validation and Error Handling
An API should never assume that a request contains valid data simply because the frontend provides a well-designed form. Clients can submit missing values, oversized strings, invalid prices, or requests that violate business rules.
Use appropriate validation for request fields and apply business validation before performing important operations.
using System.ComponentModel.DataAnnotations;
public sealed class CreateProductRequest
{
[Required]
[StringLength(150, MinimumLength = 2)]
public string Name { get; init; } = string.Empty;
[Range(0.01, 100000)]
public decimal Price { get; init; }
}
app.MapPost(
"/api/products",
(CreateProductRequest request) =>
{
if (!Validator.TryValidateObject(
request,
new ValidationContext(request),
new List<ValidationResult>(),
validateAllProperties: true))
{
return Results.BadRequest(
new { Error = "The product data is invalid." });
}
return Results.Ok(new
{
request.Name,
request.Price
});
});
This sample demonstrates explicit Data Annotations validation. It does not implement persistence, authorization, or a complete validation-error response, and it should be adapted to your application's requirements.
Validation behavior and available automatic-validation features vary by ASP.NET Core version and configuration. For larger applications, consider a reusable validation approach rather than repeating the same validation logic across every endpoint.
Configure centralized exception handling to manage unexpected failures consistently. Return safe error messages and avoid exposing stack traces, credentials, SQL statements, or internal implementation details in production responses.
See Global Exception Handling Middleware in .NET for more information about handling application errors.
9. Organizing Routes with MapGroup()
When an API contains many related endpoints, repeating the same route prefix and endpoint configuration can make the code harder to maintain. The MapGroup() method lets you organize related endpoints under a shared route prefix.
var products = app.MapGroup("/api/products");
products.MapGet("/", () =>
Results.Ok("Get all products"));
products.MapGet("/{id:int}", (int id) =>
Results.Ok(new { Id = id }));
products.MapPost("/", () =>
Results.Created(
"/api/products/1",
new { Id = 1 }));
Each endpoint belongs to the product route group. This helps keep route definitions organized as the API grows.
Groups can also apply shared conventions or metadata, including authorization requirements, endpoint filters, and tags, where appropriate. For example, an administrative route group can require authorization for all endpoints within that group.
var admin = app.MapGroup("/api/admin")
.RequireAuthorization();
admin.MapGet("/reports", () =>
Results.Ok("Administrative reports"));
admin.MapPost("/maintenance", () =>
Results.Accepted());
The authorization requirement assumes that authentication and authorization are configured for the application. Adding RequireAuthorization() does not establish a complete authentication system by itself, nor does it automatically define which authorized users may access each specific business resource.
10. Separating Endpoints from Program.cs
A minimal API can begin with a small Program.cs file, but putting every endpoint into one file becomes difficult to manage as the number of features grows. You can move endpoint definitions into dedicated classes or extension methods while preserving the Minimal API approach.
For example, create a ProductEndpoints.cs file:
public static class ProductEndpoints
{
public static IEndpointRouteBuilder MapProductEndpoints(
this IEndpointRouteBuilder app)
{
var products = app.MapGroup("/api/products");
products.MapGet("/", () =>
Results.Ok("Get all products"));
products.MapGet("/{id:int}", (int id) =>
Results.Ok(new { Id = id }));
return app;
}
}
Register the endpoints in Program.cs:
var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();
app.MapProductEndpoints();
app.Run();
This keeps route definitions close to the feature they represent without requiring controller classes. For large applications, endpoint classes can delegate to application services and use separate DTOs, validation logic, and authorization policies.
11. Authentication and Authorization
Many APIs contain both public and protected endpoints. For example, visitors might be able to browse published courses, while only authenticated learners can view private progress information or only administrators can publish new content.
Minimal APIs support authorization through endpoint conventions such as RequireAuthorization(). Authentication identifies a caller, while authorization determines whether the caller has permission to perform a specific operation.
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddAuthentication()
.AddJwtBearer();
builder.Services.AddAuthorization();
var app = builder.Build();
app.UseAuthentication();
app.UseAuthorization();
app.MapGet("/api/public-courses", () =>
Results.Ok("Public course information"));
app.MapGet("/api/my-progress", () =>
Results.Ok("Private learner progress"))
.RequireAuthorization();
app.Run();
This illustrates the general registration and middleware pattern. A production JWT configuration must specify the expected token issuer, audience, signing-key validation, and other appropriate validation settings. The parameterless registration shown here is not a complete production authentication configuration.
Authorization must also reflect the application's resource-level permissions. Merely requiring authentication does not mean that every authenticated user should be able to view or modify every record.
12. OpenAPI and API Documentation
API documentation helps frontend developers, mobile developers, testers, and other services understand available endpoints, request formats, response types, and authentication requirements.
ASP.NET Core supports OpenAPI document generation, and additional tooling can provide an interactive interface for exploring the API. The exact packages and configuration depend on your .NET version and the documentation tools you choose.
In recent ASP.NET Core versions, a basic OpenAPI setup can be configured with the framework's OpenAPI services:
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddOpenApi();
var app = builder.Build();
if (app.Environment.IsDevelopment())
{
app.MapOpenApi();
}
app.MapGet("/api/courses", () =>
Results.Ok(new[] { "C#", "ASP.NET Core" }));
app.Run();
This example uses built-in OpenAPI support available in modern ASP.NET Core versions. Interactive Swagger UI generally requires an additional package or tool and should be configured separately when needed.
Keep endpoint documentation consistent with actual behavior. Clear names, documented parameters, meaningful response metadata, and accurate authorization requirements make an API easier to integrate with and maintain.
13. Testing Minimal APIs
Minimal API endpoints should be tested for both correct results and expected failure behavior. Tests can verify HTTP status codes, request validation, authorization, response structures, and interactions with application services.
Unit tests can test business services independently from HTTP routing. Integration tests can run the API through an in-memory test host and send HTTP requests to the actual configured endpoints.
For example, an integration test for a product endpoint might verify that an existing product returns HTTP 200 and a missing product returns HTTP 404. A separate service test can verify the business logic that determines whether the product exists.
Do not rely only on successful-path tests. Include malformed requests, invalid values, missing resources, unauthorized requests, and unexpected dependency failures where they are relevant to your API.
14. Minimal APIs vs. Controller-Based APIs
Minimal APIs and controller-based APIs both use ASP.NET Core and can support production-grade HTTP services. The choice is primarily about organization, conventions, and the needs of your application rather than an assumption that one style is always faster or better.
| Area | Minimal APIs | Controller APIs |
|---|---|---|
| Endpoint definition | Direct route-handler mapping. | Controller classes and action methods. |
| Organization | Convenient for compact endpoint definitions and feature-based mappings. | Provides familiar class-based organization and controller conventions. |
| Shared behavior | Can use route groups, filters, middleware, and endpoint conventions. | Can use action filters, controller conventions, middleware, and other MVC features. |
| Typical fit | Focused APIs, smaller services, and concise route definitions. | Applications that benefit from controller conventions and MVC-specific features. |
Do not choose solely based on line count. Consider team familiarity, validation and filtering requirements, endpoint complexity, testing approach, maintainability, and the framework features your application needs.
15. Common Minimal API Mistakes
Putting all logic inside route handlers
Long handlers can become difficult to read and test. Move meaningful business operations into application services and keep endpoint code focused on HTTP behavior.
Skipping request validation
Valid JSON is not necessarily valid business input. Validate fields, enforce business rules, and return meaningful client errors.
Returning success for every outcome
Return appropriate status codes for successful operations, invalid requests, missing resources, and authentication or authorization failures.
Forgetting authorization
Protect private endpoints and enforce resource-level permissions. Public route mapping is not a security policy.
Growing an oversized Program.cs file
Group routes by feature and move endpoint registrations into separate classes or extension methods as the application grows.
Ignoring dependency lifetimes
Register services with appropriate lifetimes and avoid holding scoped dependencies inside long-lived singleton instances.
16. Best Practices for Minimal APIs
- Use clear, resource-oriented routes and appropriate HTTP methods.
- Use route constraints when they improve route matching and clarity.
- Bind request data to explicit request models rather than using loosely structured inputs for complex operations.
- Validate incoming values and enforce business invariants before modifying data.
- Return appropriate status codes and consistent error responses.
- Use dependency injection to connect endpoints to application services.
- Group related endpoints with
MapGroup()to organize routes and apply shared conventions. - Use authorization policies and resource-level permission checks for protected operations.
- Keep queries composable and use asynchronous database operations where appropriate.
- Generate and maintain accurate API documentation.
- Test endpoints, authentication, validation, and important failure scenarios.
- Move endpoint definitions into separate modules when a single file becomes difficult to maintain.
Conclusion
Minimal APIs offer a concise way to build HTTP endpoints in ASP.NET Core while retaining access to essential framework capabilities such as dependency injection, middleware, authorization, and OpenAPI integration.
Start with a few well-defined endpoints, learn how route and request binding work, and then introduce route groups, typed results, validation, and application services as your requirements grow. Minimal APIs do not require minimal engineering discipline: production endpoints still need security, consistent errors, automated tests, and maintainable business logic.
Continue learning with REST API Design Best Practices in .NET, Dependency Injection, AppSettings and Configuration, Data Access in .NET, and .NET Development Best Practices.