| name | dotnet-api-endpoint |
| description | Create ASP.NET Core Minimal API endpoints with proper routing, validation, and error handling. Default HTTP skill for this repository (src/Api). Use when user says "add endpoint", "create API route", "new endpoint", "REST API", "minimal API", "map route", or asks to expose functionality via HTTP. Do NOT use for domain logic or infrastructure. Do NOT use dotnet-mvc for the same work — API-only and Minimal API projects should never load dotnet-mvc unless the user explicitly adds MVC/Razor. |
| metadata | {"author":"Claude-DotNet-Ultimate","version":"1.1.0","category":"api-layer"} |
Minimal API Endpoint Creation
This is the default HTTP/surface skill for this repo. All new routes go in src/Api/Endpoints/ as Minimal APIs. If the user chose API only (not MVC), use this skill — not dotnet-mvc.
File Location
All endpoints go in src/Api/Endpoints/:
Endpoints/
├── OrdersEndpoints.cs
├── ProductsEndpoints.cs
└── CustomersEndpoints.cs
Creating an Endpoint Group
namespace ClaudeDotNetUltimate.Api.Endpoints;
public static class ProductsEndpoints
{
public static void MapProductsEndpoints(this IEndpointRouteBuilder app)
{
RouteGroupBuilder group = app.MapGroup("/api/products")
.WithTags("Products")
.WithOpenApi();
group.MapPost("/", CreateProduct)
.WithName("CreateProduct")
.Produces<ProductDto>(StatusCodes.Status201Created)
.ProducesValidationProblem()
.ProducesProblem(StatusCodes.Status500InternalServerError);
group.MapGet("/{productId:guid}", GetProduct)
.WithName("GetProduct")
.Produces<ProductDto>()
.ProducesProblem(StatusCodes.Status404NotFound);
}
private static async Task<IResult> CreateProduct(
CreateProductCommand command,
IMediator mediator,
CancellationToken ct)
{
Result<ProductDto> result = await mediator.Send(command, ct);
return result.IsSuccess
? Results.Created($"/api/products/{result.Value!.Id}", result.Value)
: MapError(result.Error!);
}
private static async Task<IResult> GetProduct(
Guid productId,
IMediator mediator,
CancellationToken ct)
{
Result<ProductDto> result = await mediator.Send(new GetProductQuery(productId), ct);
return result.IsSuccess
? Results.Ok(result.Value)
: MapError(result.Error!);
}
private static IResult MapError(Error error) => error.Type switch
{
ErrorType.NotFound => Results.Problem(
title: error.Code, detail: error.Message, statusCode: 404),
ErrorType.Validation => Results.Problem(
title: error.Code, detail: error.Message, statusCode: 400),
ErrorType.Unauthorized => Results.Problem(
title: error.Code, detail: error.Message, statusCode: 401),
ErrorType.Forbidden => Results.Problem(
title: error.Code, detail: error.Message, statusCode: 403),
ErrorType.BusinessRuleViolation => Results.Problem(
title: error.Code, detail: error.Message, statusCode: 422),
ErrorType.ConcurrencyConflict => Results.Problem(
title: error.Code, detail: error.Message, statusCode: 409),
_ => Results.Problem(
title: error.Code, detail: error.Message, statusCode: 500)
};
}
Register in Program.cs
app.MapProductsEndpoints();
Endpoint Patterns
POST (Create)
- Return
201 Created with Location header
- Body is the command record (auto-deserialized)
- Validation handled by MediatR pipeline
GET (Read)
- Route parameter
{id:guid} for type safety
- Return
200 OK or 404 Not Found
- Cache-first via query handler
PUT (Update)
- Route parameter for ID, body for update data
- Return
200 OK or 404 Not Found
- Invalidate cache after update
DELETE
- Route parameter for ID
- Return
204 No Content or 404 Not Found
Rules
- All endpoints return
IResult using Results.* factory methods
- Errors mapped to
ProblemDetails via Results.Problem()
- Use
WithOpenApi() for Swagger documentation
- Use
WithTags() for API grouping
- Use
WithName() for link generation
- Route parameters use type constraints (
:guid, :int)
CancellationToken always passed to mediator
- Static methods for endpoint handlers (no state)
Error Mapping
| ErrorType | HTTP Status |
|---|
| NotFound | 404 |
| Validation | 400 |
| Unauthorized | 401 |
| Forbidden | 403 |
| BusinessRuleViolation | 422 |
| ConcurrencyConflict | 409 |
| Failure (default) | 500 |