Create a new API endpoint with Command, Handler, Validator, and Endpoint following FSH vertical slice architecture. Use when adding any new feature, API endpoint, or business operation.
Create a complete vertical slice feature with all required files.
src/Modules/{Module}/Features/v1/{FeatureName}/
├── {Action}{Entity}Command.cs # or Get{Entity}Query.cs
├── {Action}{Entity}Handler.cs
├── {Action}{Entity}Validator.cs # Commands only
└── {Action}{Entity}Endpoint.cs
For state changes (POST/PUT/DELETE):
public sealed record Create{Entity}Command(
string Name,
decimal Price) : ICommand<Create{Entity}Response>;
For reads (GET):
public sealed record Get{Entity}Query(Guid Id) : IQuery<{Entity}Dto>;
public sealed class Create{Entity}Handler(
IRepository<{Entity}> repository,
ICurrentUser currentUser) : ICommandHandler<Create{Entity}Command, Create{Entity}Response>
{
public async ValueTask<Create{Entity}Response> Handle(
Create{Entity}Command command,
CancellationToken ct)
{
var entity = {Entity}.Create(command.Name, command.Price, currentUser.TenantId);
await repository.AddAsync(entity, ct);
return new Create{Entity}Response(entity.Id);
}
}
public sealed class Create{Entity}Validator : AbstractValidator<Create{Entity}Command>
{
public Create{Entity}Validator()
{
RuleFor(x => x.Name).NotEmpty().MaximumLength(200);
RuleFor(x => x.Price).GreaterThan(0);
}
}
public static class Create{Entity}Endpoint
{
public static RouteHandlerBuilder Map(this IEndpointRouteBuilder endpoints) =>
endpoints.MapPost("/", async (
Create{Entity}Command command,
IMediator mediator,
CancellationToken ct) => TypedResults.Created(
$"/{entities}/{(await mediator.Send(command, ct)).Id}"))
.WithName(nameof(Create{Entity}Command))
.WithSummary("Create a new {entity}")
.RequirePermission({Module}Permissions.{Entities}.Create);
}
In src/Modules/{Module}/Modules.{Module}.Contracts/:
public sealed record Create{Entity}Response(Guid Id);
public sealed record {Entity}Dto(Guid Id, string Name, decimal Price);
In {Module}Module.cs MapEndpoints method:
var entities = endpoints.MapGroup("/{entities}").WithTags("{Entities}");
entities.Map{Action}{Entity}Endpoint();
dotnet build src/FSH.Starter.slnx # Must be 0 warnings
dotnet test src/FSH.Starter.slnx
ICommand<T> or IQuery<T> (NOT MediatR's IRequest)ICommandHandler<T,R> or IQueryHandler<T,R>ValueTask<T> (NOT Task<T>).RequirePermission() or .AllowAnonymous().WithName() and .WithSummary()