Why it's built this way
Every template is a pile of opinions. Here are ours, so you can disagree with them on purpose rather than by accident.
Controllers, with one base class
Section titled “Controllers, with one base class”Each feature is one controller that inherits ApiControllerBase, which carries [ApiController], [Authorize], [Consumes("application/json")] and the Mediator Sender. A new action is an attribute, a method and a Send, and it can’t forget to be protected. If you’d rather skip controllers altogether, Clean Minimal API is the same template without them.
Built-in OpenAPI and Scalar, not Swagger
Section titled “Built-in OpenAPI and Scalar, not Swagger”The OpenAPI document comes from Microsoft.AspNetCore.OpenApi, which reads the XML comments you were going to write anyway. Summaries, remarks, parameter docs and response descriptions all flow through, and so do the <param> tags on the Application records, so the schemas are documented too. Scalar renders it, with a request builder that beats a wall of curl. Swashbuckle was fine, but this needs one less package and reads the comments properly.
Problem Details for every error
Section titled “Problem Details for every error”Every failure, from a bad limit to a missing movie to an unhandled exception, comes back as RFC 9457 Problem Details with the right status code. One exception handler does the mapping. Clients get one error shape to parse, and nobody has to read a stack trace in a JSON string.
Application entities are the response
Section titled “Application entities are the response”The endpoints return the Application layer’s records as they are, serialised in camelCase. There’s no second set of DTOs to keep in step, because the entities already are the contract: a MovieSummary for lists, a Movie with its reviews for a single lookup. The only Presentation models are the two request bodies for writing reviews. If a client ever needs a different shape, that’s the moment to add a mapping, not before.
Source generators where it counts
Section titled “Source generators where it counts”- Mediator generates its dispatch code at build time, so sending a query is a method call, not a reflection hunt.
- Mapperly generates the Infrastructure mapping at build time, and you can read exactly what it wrote.
- System.Text.Json is configured once in
AddJsonOptions: camelCase, case-insensitive, nulls left out and enums as strings. As a bonus, MediatR and AutoMapper both moved to commercial licences in 2025, and this template didn’t have to care.
FluentValidation in a pipeline behaviour
Section titled “FluentValidation in a pipeline behaviour”Validation runs once, in one place, before any handler. Handlers only ever see valid requests, which keeps them short and keeps tests honest. Presentation just has to turn a ValidationException into a 400 Problem Details response, and it does that in one class.
Optional authentication, mandatory authorization
Section titled “Optional authentication, mandatory authorization”Every endpoint asks for authorization. Whether that means a valid JWT bearer token or a polite wave through depends on one setting, Authentication:Authority. Turning security on is configuration, not a code change, and turning it off for a demo doesn’t leave an endpoint behind. See Secure the API.
SQLite next to the app
Section titled “SQLite next to the app”The template needs a database that works in a container and on a laptop without any setup. SQLite does that, with real caveats. It’s there to show the shape of Infrastructure, not to be your production database.
Infrastructure is internal
Section titled “Infrastructure is internal”The one rule we bend is letting Presentation reference Infrastructure for dependency injection. Making every Infrastructure class internal means the compiler enforces the boundary instead of a code reviewer having a bad day.
Strict builds
Section titled “Strict builds”Directory.Build.props turns on code style and the recommended analyzers for every project, and warnings fail the build. Directory.Packages.props keeps every package version in one place. It’s annoying for about a day and then you never want to go back.
Tests that don’t share
Section titled “Tests that don’t share”Every test class gets its own in-memory SQLite database, built from the real migrations and the same fixed seed. Tests can run in any order, in parallel, forever, and still agree with each other.