| name | add-endpoint |
| description | Exact shape of a new minimal API endpoint in Web.Api — IEndpoint, request DTO, handler wiring, Result matching. Use when adding or modifying files under src/Web.Api/Endpoints. |
File layout
One file per endpoint under src/Web.Api/Endpoints/<Feature>/<Action>.cs:
src/Web.Api/Endpoints/Todos/
Create.cs
Get.cs
GetById.cs
Complete.cs
Delete.cs
Shape — command endpoint
using Application.Abstractions.Messaging;
using Application.Todos.Create;
using Domain.Todos;
using SharedKernel;
using Web.Api.Extensions;
using Web.Api.Infrastructure;
namespace Web.Api.Endpoints.Todos;
internal sealed class Create : IEndpoint
{
public sealed class Request
{
public Guid UserId { get; set; }
public string Description { get; set; }
public DateTime? DueDate { get; set; }
public List<string> Labels { get; set; } = [];
public int Priority { get; set; }
}
public void MapEndpoint(IEndpointRouteBuilder app)
{
app.MapPost("todos", async (
Request request,
ICommandHandler<CreateTodoCommand, Guid> handler,
CancellationToken cancellationToken) =>
{
var command = new CreateTodoCommand
{
UserId = request.UserId,
Description = request.Description,
DueDate = request.DueDate,
Labels = request.Labels,
Priority = (Priority)request.Priority
};
Result<Guid> result = await handler.Handle(command, cancellationToken);
return result.Match(Results.Ok, CustomResults.Problem);
})
.WithTags(Tags.Todos)
.RequireAuthorization();
}
}
Shape — query endpoint
app.MapGet("todos/{todoItemId:guid}", async (
Guid todoItemId,
IQueryHandler<GetTodoByIdQuery, TodoResponse> handler,
CancellationToken cancellationToken) =>
{
var query = new GetTodoByIdQuery(todoItemId);
Result<TodoResponse> result = await handler.Handle(query, cancellationToken);
return result.Match(Results.Ok, CustomResults.Problem);
})
.WithTags(Tags.Todos)
.RequireAuthorization();
Rules
- Class is
internal sealed, implements IEndpoint. No base class.
- Nested
public sealed class Request for request bodies — keep it next to the endpoint, don't put request DTOs in Application.
- Inject handlers via
ICommandHandler<TCommand, TResult> / IQueryHandler<TQuery, TResult> parameters in the route delegate. Never inject MediatR.
- Terminate every route with
result.Match(Results.Ok, CustomResults.Problem) — CustomResults.Problem turns a Result failure into a ProblemDetails response.
- Apply
.WithTags(Tags.<Feature>) for OpenAPI grouping. If the feature is new, add a constant to src/Web.Api/Endpoints/Tags.cs.
- Apply
.RequireAuthorization() unless the endpoint is explicitly anonymous (login / register). Use [HasPermission("...")] or .RequireAuthorization("<policy>") for permission-scoped routes — see src/Infrastructure/Authorization/.
Registration
Endpoints are discovered automatically by EndpointExtensions.MapEndpoints via assembly scanning for IEndpoint. Do not edit Program.cs when adding an endpoint.
Don'ts
- Don't put business logic in the endpoint — it only maps a
Request to a Command/Query and matches the Result.
- Don't return custom
IResult types from the handler side — keep HTTP concerns inside the endpoint file.
- Don't call a DbContext or Infrastructure type from an endpoint — go through a handler.