Vertical slices, not layers
An operation is one folder: its contract, its handler, its permissions, its tests. Changing an endpoint means opening one directory.

Pre-release · net10.0 · MIT
One class per endpoint, carrying its route, its metadata, and its handling. Ordinary ASP.NET Core underneath, all the way down.
The API is still settling and will move before 1.0. Prereleases are published to GitHub Packages; nuget.org opens once the documentation is complete and the API has held still across two consecutive milestones.
An endpoint
The attribute declares the route. The namespace supplies the operation id (InvoicesGet), so nothing has to be named twice. The request record is bound from the route, and the response is serialized and documented.
namespace Billing.Endpoints.Invoices.Get;
public sealed record GetInvoice(string InvoiceId);
[Get("invoices/{invoiceId}")]
public sealed class Endpoint(IInvoiceStore store) : ApiEndpoint<GetInvoice, InvoiceView>
{
public override async Task<InvoiceView> HandleAsync(GetInvoice request, CancellationToken ct) =>
InvoiceView.From(await store.GetAsync(request.InvoiceId, ct));
}var builder = WebApplication.CreateBuilder(args);
builder.Services.AddNativeEndpoints();
var app = builder.Build();
app.MapEndpointGroup().MapEndpointsFrom(typeof(Program).Assembly, routePrefix: "/api");
app.Run();Why
Past a few dozen routes you are choosing between a Program.cs nobody wants to open, a pile of extension methods that hide the route table, or a framework that replaces ASP.NET Core with its own parallel universe. NativeEndpoints takes the middle path: it gives you a place to put an endpoint and takes nothing away.
An operation is one folder: its contract, its handler, its permissions, its tests. Changing an endpoint means opening one directory.
Routing, filters, results, CORS, rate limiting, output caching, authorization and OpenAPI stay ASP.NET Core's. Every escape hatch is an IEndpointConventionBuilder.
HandleAsync is a method. Call a service, query a store, dispatch to whatever you already use. The framework owns route, binding and metadata, and stops there.
API Explorer needs a MethodInfo in metadata or your endpoint silently vanishes from the OpenAPI document. Handled once, for every endpoint.
Base types
ApiEndpointWithResult covers operations whose status depends on what happened. The documented schema stays the response type; the wrapper never reaches the wire.
ApiEndpoint<TRequest, TResponse>A request in, a response body outApiEndpoint<TRequest>A request in, 204 No Content outApiEndpointWithoutRequest<TResponse>No contract, a response body outApiEndpointWithResult<TRequest, TResponse>The status code is decided by the handlerApiEndpointBaseWrite the response yourselfpublic override async Task<EndpointResult<InvoiceView>> HandleAsync(
CreateInvoice cmd, CancellationToken ct)
{
var (invoice, created) = await store.UpsertAsync(cmd, ct);
return created
? EndpointResult.Status(StatusCodes.Status201Created, InvoiceView.From(invoice))
: EndpointResult.Ok(InvoiceView.From(invoice));
}Binding
Route wins over the body so a resource identifier in the URL cannot be contradicted by the payload. Built in: string, bool, int, long, Guid, enum, DateTimeOffset, anything implementing IParsable<T>, arrays and lists of those from the query string, plus headers and claims.
NE0004: Contract 'Transfer' has parameter 'amount' of unsupported type 'Money'.
Register a value binder or use a supported type.Anything unsupported throws, loudly, rather than binding silently to a default. Body handling is explicit per endpoint via options.BodyMode: None, Optional, Required, or RequiredWithContentType, the last rejecting a non-JSON content type with a bare 415 before the body is read.
This is a design position, not a gap: predictable binding you can hold in your head, and a loud failure instead of a quiet one.
Errors
Domain exceptions become responses through translators you register, rather than a global filter every part of the application has to agree on. Problems are written as RFC 9457 ProblemDetails through IProblemDetailsService.
public sealed class BillingExceptionTranslator : IEndpointExceptionTranslator
{
public EndpointProblem? Translate(Exception exception) => exception switch
{
InvoiceNotFoundException => EndpointProblem.General(404, "Invoice not found"),
InvoiceLockedException e => EndpointProblem.General(409, e.Message),
_ => null
};
}Unload safety
If you host plugins in collectible AssemblyLoadContexts, endpoint frameworks are usually where unloading goes to die. Process-global registries, static configuration and captured handler MethodInfo all root the assembly you are trying to release — and none of it is visible until you measure.
RequestDelegate, keeping your async state machine out of retained metadata.[Fact]
public async Task Module_unloads()
{
var evidence = await CollectibleEndpointFixture.RunCyclesAsync(cycles: 3);
UnloadEvidence.Verify(evidence, gcRounds: 32);
}0 of 3
collectible contexts collected with FastEndpoints 7.2.0 in a harness that compiled three endpoint assemblies, served a request, disposed the host and forced repeated full collections. That isolates a composition-level retention problem; it is not a claim about FastEndpoints in any other respect. It is the reason this library exists.
Compared to FastEndpoints
FastEndpoints is a mature, popular and genuinely good library, and it does considerably more than this one. If you want a batteries-included framework, use it.
What it does not do
Not in 1.0. Use a plain MapPost beside your endpoints.
Bring FluentValidation, DataAnnotations, or hand-written guards.
net10.0 only. A new library targeting .NET 8 would ship dead code.