| name | dotnet-minimal-apis |
| description | Builds ASP.NET Core Minimal APIs -- route groups, filters, TypedResults, OpenAPI. |
| license | MIT |
| targets | ["*"] |
| category | web |
| subcategory | minimal-apis |
| tags | ["web","dotnet","skill","minimal-apis","api"] |
| version | 1.0.0 |
| author | dotnet-agent-harness |
| invocable | true |
| related_skills | ["dotnet-architecture-patterns","dotnet-middleware-patterns","dotnet-api-versioning"] |
| claudecode | {"allowed-tools":["Read","Grep","Glob","Bash","Write","Edit"]} |
| codexcli | {"short-description":".NET skill guidance for web tasks"} |
| opencode | {"allowed-tools":["Read","Grep","Glob","Bash","Write","Edit"]} |
| copilot | {} |
| geminicli | {} |
| antigravity | {} |
dotnet-minimal-apis
Minimal APIs are Microsoft's recommended approach for new ASP.NET Core HTTP API projects. They provide a lightweight,
lambda-based programming model with first-class OpenAPI support, endpoint filters for cross-cutting concerns, and route
groups for organization at scale.
Scope
- Route groups and endpoint organization
- Endpoint filters for cross-cutting concerns
- TypedResults for compile-time response type safety
- Parameter binding (route, query, body, services)
- JSON configuration with ConfigureHttpJsonOptions
- Carter library integration for auto-discovery modules
Out of scope
- API versioning strategies -- see [skill:dotnet-api-versioning]
- Input validation frameworks -- see [skill:dotnet-input-validation]
- Architectural patterns (vertical slices, CQRS) -- see [skill:dotnet-architecture-patterns]
- Authentication and authorization -- see [skill:dotnet-api-security]
- OpenAPI document generation -- see [skill:dotnet-openapi]
- gRPC and real-time communication -- see [skill:dotnet-grpc] and [skill:dotnet-realtime-communication]
Cross-references: [skill:dotnet-architecture-patterns] for organizing large APIs, [skill:dotnet-input-validation] for
request validation, [skill:dotnet-api-versioning] for versioning strategies, [skill:dotnet-openapi] for OpenAPI
customization.
Route Groups
Route groups organize related endpoints under a shared prefix, applying common configuration (filters, metadata,
authorization) once. They replace repetitive chaining of MapGet/MapPost with shared prefixes.
var app = builder.Build();
var products = app.MapGroup("/api/products")
.WithTags("Products")
.RequireAuthorization();
products.MapGet("/", async (AppDbContext db) =>
TypedResults.Ok(await db.Products.ToListAsync()));
products.MapGet("/{id:int}", async (int id, AppDbContext db) =>
await db.Products.FindAsync(id) is Product product
? TypedResults.Ok(product)
: TypedResults.NotFound());
products.MapPost(, (CreateProductDto dto, AppDbContext db) =>
{
product = Product { Name = dto.Name, Price = dto.Price };
db.Products.Add(product);
db.SaveChangesAsync();
TypedResults.Created(, product);
});
products.MapDelete(, ( id, AppDbContext db) =>
{
( db.Products.FindAsync(id) Product product)
TypedResults.NotFound();
db.Products.Remove(product);
db.SaveChangesAsync();
TypedResults.NoContent();
});
```text
Groups can be nested to compose prefixes filters:
```csharp
api = app.MapGroup()
.AddEndpointFilter<RequestLoggingFilter>();
v1 = api.MapGroup();
products = v1.MapGroup().WithTags();
orders = v1.MapGroup().WithTags();
products.MapGet(, GetProducts);
orders.MapPost(, CreateOrder);
```text
---
Endpoint filters provide a pipeline cross-
{
ValueTask<?> InvokeAsync(
EndpointFilterInvocationContext context,
EndpointFilterDelegate next)
{
argument = context.Arguments
.OfType<T>()
.FirstOrDefault();
(argument )
TypedResults.BadRequest();
result = validator.ValidateAsync(argument);
(!result.IsValid)
{
TypedResults.ValidationProblem(
result.ToDictionary());
}
next(context);
}
}
```text
```csharp
products.MapPost(, CreateProduct)
.AddEndpointFilter<ValidationFilter<CreateProductDto>>();
products = app.MapGroup()
.AddEndpointFilter<RequestLoggingFilter>();
products.MapGet(, GetProductById)
.AddEndpointFilter( (context, next) =>
{
id = context.GetArgument<>();
(id <= )
TypedResults.BadRequest();
next(context);
});
```text
=>
db.Products.FindAsync(id) Product product
? TypedResults.Ok(product)
: TypedResults.NotFound());
products.MapGet(, ( id, AppDbContext db) =>
db.Products.FindAsync(id) Product product
? Results.Ok(product)
: Results.NotFound());
```text
Use `Results<T1, T2, ...>` to declare all possible response types a single endpoint. This enables accurate OpenAPI
documentation multiple response codes:
```csharp
products.MapPost(, =>
{
( db.Products.AnyAsync(p => p.Sku == dto.Sku))
TypedResults.Conflict();
product = Product { Name = dto.Name, Sku = dto.Sku, Price = dto.Price };
db.Products.Add(product);
db.SaveChangesAsync();
TypedResults.Created(, product);
});
```text
---
.NET adds built- OpenAPI support via `Microsoft.AspNetCore.OpenApi`. Minimal APIs generate OpenAPI metadata
`TypedResults`, parameter bindings, attributes automatically.
```csharp
builder.Services.AddOpenApi();
app = builder.Build();
(app.Environment.IsDevelopment())
{
app.MapOpenApi();
}
```json
```csharp
products.MapGet(, GetProductById)
.WithName()
.WithSummary()
.WithDescription()
.Produces<Product>(StatusCodes.Status200OK)
.ProducesProblem(StatusCodes.Status404NotFound);
```
{
{
= routes.MapGroup()
.WithTags();
.MapGet(, GetAll);
.MapGet(, GetById);
.MapPost(, Create);
.MapPut(, Update);
.MapDelete(, Delete);
;
}
Task<Ok<List<Product>>> GetAll(AppDbContext db) =>
TypedResults.Ok( db.Products.ToListAsync());
Task<Results<Ok<Product>, NotFound>> GetById(
id, AppDbContext db) =>
db.Products.FindAsync(id) Product p
? TypedResults.Ok(p)
: TypedResults.NotFound();
Task<Created<Product>> Create(
CreateProductDto dto, AppDbContext db)
{
product = Product { Name = dto.Name, Price = dto.Price };
db.Products.Add(product);
db.SaveChangesAsync();
TypedResults.Created(, product);
}
Task<Results<NoContent, NotFound>> Update(
id, UpdateProductDto dto, AppDbContext db)
{
product = db.Products.FindAsync(id);
(product ) TypedResults.NotFound();
product.Name = dto.Name;
product.Price = dto.Price;
db.SaveChangesAsync();
TypedResults.NoContent();
}
Task<Results<NoContent, NotFound>> Delete(
id, AppDbContext db)
{
product = db.Products.FindAsync(id);
(product ) TypedResults.NotFound();
db.Products.Remove(product);
db.SaveChangesAsync();
TypedResults.NoContent();
}
}
app.MapProductEndpoints();
app.MapOrderEndpoints();
app.MapCustomerEndpoints();
```csharp
For projects that prefer auto-discovery of endpoint modules, the Carter library provides an `ICarterModule` :
```
:
{
{
= app.MapGroup().WithTags();
.MapGet(, (AppDbContext db) =>
TypedResults.Ok( db.Products.ToListAsync()));
.MapGet(, ( id, AppDbContext db) =>
db.Products.FindAsync(id) Product p
? TypedResults.Ok(p)
: TypedResults.NotFound());
}
}
builder.Services.AddCarter();
app = builder.Build();
app.MapCarter();
```csharp
;
});
```json
**Gotcha:** `ConfigureHttpJsonOptions` configures JSON serialization Minimal APIs only. MVC controllers use a
separate pipeline -- configure via `builder.Services.AddControllers().AddJsonOptions(...)`. Mixing them up has no
effect.
---
Minimal APIs bind parameters route, query, headers, body, DI automatically based type attribute
annotations.
```csharp
app.MapGet(, ( id) => ...);
app.MapGet(, ([FromQuery] page, [FromQuery] pageSize) => ...);
app.MapGet(, ([FromHeader(Name = )] correlationId) => ...);
app.MapPost(, (CreateProductDto dto) => ...);
app.MapGet(, (AppDbContext db, ILogger<Program> logger) => ...);
app.MapGet(, ([AsParameters] ProductQuery query) => ...);
;
```text
---
**Do use `Results` `TypedResults` available** -- `Results.Ok()` returns `IResult` the OpenAPI
generator cannot infer response schemas. Use `TypedResults.Ok()` to enable automatic schema generation.
**Do forget `ConfigureHttpJsonOptions` only applies to Minimal APIs** -- MVC controllers need
`.AddControllers().AddJsonOptions()` separately.
**Do apply validation logic inline every endpoint** -- use endpoint filters cross-reference
[] centralized validation patterns.
**Do register filters the wrong order** -- first-registered filter outermost.