CtrlK
BlogDocsLog inGet started
Tessl Logo

minimal-apis

Use when writing, editing, or reviewing ASP.NET Core Minimal API endpoints, route handlers, route groups, endpoint filters, or API responses; configuring OpenAPI document generation or build-time spec export; implementing endpoint validation or problem details error handling; using Server-Sent Events for streaming; writing integration tests against API endpoints; embedding Kestrel in a CLI tool. Triggers on phrases like "add an endpoint", "minimal API", "route group", "endpoint filter", "typed results", "OpenAPI spec", "Kestrel server", "API endpoint", "HTTP server endpoints". Do NOT use for MVC controllers, Razor Pages, Blazor Server, or SignalR hubs.

68

Quality

82%

Does it follow best practices?

Run evals on this skill

Adds up to 20 points to the overall score

View guide

SecuritybySnyk

Passed

No findings from the security scan

SKILL.md
Quality
Evals
Security

Minimal APIs (.NET 10)

Best practices for ASP.NET Core Minimal APIs targeting .NET 10. Minimal APIs are Microsoft's recommended approach for new HTTP API projects, offering simplified syntax, better performance, and reduced overhead compared to controller-based APIs.

Core Principles

  • Use WebApplication.CreateSlimBuilder() for embedded servers (excludes IIS, HTTPS dev cert, EventLog). Use CreateBuilder() only when the full feature set is needed.
  • Organize endpoints into static extension method classes, one per feature area. Each class maps a RouteGroupBuilder with a shared prefix.
  • Use TypedResults (not Results) for all return values. TypedResults provides compile-time type safety, automatic OpenAPI metadata, and testable concrete types.
  • Use Results<T1, T2, ...TN> union return types to declare all possible responses. The compiler enforces that only declared result types are returned.
  • Use [AsParameters] to group handler parameters into records or structs, reducing parameter bloat.

Endpoint Organization

Structure endpoints as static classes with extension methods on IEndpointRouteBuilder. One class per feature area, one file per class.

// SyncEndpoints.cs
public static class SyncEndpoints
{
    public static RouteGroupBuilder MapSyncEndpoints(this IEndpointRouteBuilder app)
    {
        var group = app.MapGroup("/api/sync");

        group.MapPost("/", TriggerSync);
        group.MapGet("/jobs/{id}", GetJobStatus);

        return group;
    }

    private static async Task<Results<Accepted<JobReference>, ValidationProblem>> TriggerSync(
        [AsParameters] SyncRequest request,
        ISyncPort syncPort,
        CancellationToken ct)
    {
        // Handler implementation
    }

    private static async Task<Results<Ok<JobStatus>, NotFound>> GetJobStatus(
        Guid id,
        IJobTracker tracker)
    {
        // Handler implementation
    }
}

Register in Program.cs with one line per feature area:

app.MapSyncEndpoints();
app.MapConfigEndpoints();
app.MapCustomFormatEndpoints();

Route Groups

Use MapGroup for shared prefixes, filters, and metadata:

var api = app.MapGroup("/api")
    .AddEndpointFilter<RequestLoggingFilter>()
    .ProducesProblem(StatusCodes.Status500InternalServerError);

api.MapGroup("/sync").MapSyncEndpoints();
api.MapGroup("/config").MapConfigEndpoints();

Groups support nesting. Filters and metadata applied to a group affect all endpoints in that group.

TypedResults and Response Types

MUST use TypedResults over Results for all return values. TypedResults automatically provides OpenAPI response metadata and returns strongly typed objects for unit testing.

// Correct: TypedResults with union return type
private static async Task<Results<Ok<SyncResult>, NotFound, Accepted<JobReference>>> Handle(...)
{
    return condition switch
    {
        true => TypedResults.Ok(result),
        false => TypedResults.NotFound(),
        _ => TypedResults.Accepted($"/api/jobs/{jobId}", new JobReference(jobId))
    };
}

Available TypedResults factory methods (non-exhaustive):

MethodStatusUse case
Ok<T>()200Successful response with body
Created<T>()201Resource created
Accepted<T>()202Long-running operation started
NoContent()204Success, no body
BadRequest<T>()400Invalid request
NotFound()404Resource not found
Problem()500RFC 9457 problem details
ValidationProblem()400Validation errors
InternalServerError()500Server error
ServerSentEvents()200SSE stream

Parameter Binding

[AsParameters] for grouped parameters

Group related parameters into a record or struct to reduce handler signature bloat:

public record SyncRequest(
    [FromBody] SyncRequestBody Body,
    [FromQuery] bool? Preview,
    [FromHeader(Name = "X-Request-Id")] string? RequestId);

app.MapPost("/api/sync", async ([AsParameters] SyncRequest request, ISyncPort port) => ...);

Binding sources

Parameters bind automatically based on HTTP method and type:

  • Route values: {id} in the route template
  • Query string: Primitive types for GET/HEAD/OPTIONS/DELETE
  • Body (JSON): Complex types for POST/PUT/PATCH
  • DI services: Types registered in the DI container (no [FromServices] needed)
  • Special types: HttpContext, HttpRequest, HttpResponse, CancellationToken, ClaimsPrincipal bind automatically

Use explicit attributes ([FromRoute], [FromQuery], [FromHeader], [FromBody], [FromForm]) when the default binding source is not what you want.

Endpoint Filters

Endpoint filters are the Minimal API equivalent of MVC action filters. They run before and after the handler and can inspect/modify parameters and responses.

// Filter as a class implementing IEndpointFilter
public class RequestLoggingFilter(ILogger<RequestLoggingFilter> logger) : IEndpointFilter
{
    public async ValueTask<object?> InvokeAsync(
        EndpointFilterInvocationContext context,
        EndpointFilterDelegate next)
    {
        logger.LogInformation("Request: {Method} {Path}",
            context.HttpContext.Request.Method,
            context.HttpContext.Request.Path);

        var result = await next(context);

        return result;
    }
}

// Apply to a group or individual endpoint
group.AddEndpointFilter<RequestLoggingFilter>();

Filters resolve dependencies from DI. Execution order follows registration order (FIFO before handler, FILO after handler).

Validation (.NET 10)

.NET 10 adds built-in validation support for Minimal APIs using DataAnnotations:

builder.Services.AddValidation();

This automatically validates parameters annotated with [Required], [Range], [StringLength], etc. Validation failures return 400 Bad Request with problem details.

Works with both classes and records:

public record CreateSyncRequest(
    [Required] string InstanceName,
    [Range(1, 3600)] int TimeoutSeconds);

Customize error responses via IProblemDetailsService. Disable validation on specific endpoints with .DisableValidation().

OpenAPI

Runtime document generation

builder.Services.AddOpenApi();
// ...
app.MapOpenApi(); // Serves at /openapi/v1.json by default

The Microsoft.AspNetCore.OpenApi package generates OpenAPI 3.1 documents with JSON Schema 2020-12. Supports both Minimal APIs and controllers.

Build-time document generation

Add Microsoft.Extensions.ApiDescription.Server for build-time spec output:

<ItemGroup>
  <PackageReference Include="Microsoft.AspNetCore.OpenApi" />
  <PackageReference Include="Microsoft.Extensions.ApiDescription.Server"
                    PrivateAssets="all" />
</ItemGroup>

<PropertyGroup>
  <OpenApiGenerateDocuments>true</OpenApiGenerateDocuments>
  <OpenApiDocumentsDirectory>$(MSBuildProjectDirectory)/../openapi</OpenApiDocumentsDirectory>
</PropertyGroup>

Build-time generation launches the app's entry point with a mock server. Guard startup code that requires live dependencies:

if (Assembly.GetEntryAssembly()?.GetName().Name != "GetDocument.Insider")
{
    // Code that requires database connections, etc.
}

XML doc comments in OpenAPI

.NET 10 supports pulling /// XML doc comments into the OpenAPI document. Enable in the project file:

<GenerateDocumentationFile>true</GenerateDocumentationFile>

OpenAPI YAML

.NET 10 supports serving OpenAPI in YAML format:

app.MapOpenApi("/openapi/{documentName}.yaml");

API UI with Scalar

Microsoft recommends Scalar over Swagger UI for interactive API documentation:

using Scalar.AspNetCore;

if (app.Environment.IsDevelopment())
{
    app.MapOpenApi();
    app.MapScalarApiReference(); // Available at /scalar
}

OpenAPI document transformers

Customize the generated document with transformer APIs:

builder.Services.AddOpenApi(options =>
{
    options.AddDocumentTransformer((document, context, ct) =>
    {
        document.Info.Title = "Recyclarr API";
        document.Info.Version = "v1";
        return Task.CompletedTask;
    });
});

Server-Sent Events (.NET 10)

.NET 10 adds TypedResults.ServerSentEvents for streaming event data:

app.MapGet("/api/sync/progress/{jobId}", (Guid jobId, IJobTracker tracker) =>
{
    async IAsyncEnumerable<SseItem<ProgressUpdate>> StreamProgress(
        [EnumeratorCancellation] CancellationToken ct)
    {
        await foreach (var update in tracker.GetProgressStream(jobId, ct))
        {
            yield return SseItem.Create(update, eventType: "progress");
        }
    }

    return TypedResults.ServerSentEvents(StreamProgress());
});

The client receives events over a single HTTP connection with Content-Type: text/event-stream.

Error Handling

Problem Details (RFC 9457)

Register problem details service for consistent error responses:

builder.Services.AddProblemDetails();

This ensures all error responses (including framework-generated 404s, 500s, etc.) follow the Problem Details format.

Exception handling

Use UseExceptionHandler middleware or UseStatusCodePages for global error handling:

app.UseExceptionHandler(); // Uses IProblemDetailsService when registered
app.UseStatusCodePages();  // Produces Problem Details for empty error responses

Testing

Unit testing handlers

Static handler methods with TypedResults return types are directly testable:

[Test]
public async Task GetJob_WhenNotFound_ReturnsNotFound()
{
    var tracker = Substitute.For<IJobTracker>();
    tracker.GetJob(Arg.Any<Guid>()).Returns((JobStatus?)null);

    var result = await SyncEndpoints.GetJobStatus(Guid.NewGuid(), tracker);

    result.Result.Should().BeOfType<NotFound>();
}

Integration testing with WebApplicationFactory

public class SyncApiTests : IClassFixture<WebApplicationFactory<Program>>
{
    private readonly HttpClient _client;

    public SyncApiTests(WebApplicationFactory<Program> factory)
    {
        _client = factory.CreateClient();
    }

    [Test]
    public async Task Health_ReturnsOk()
    {
        var response = await _client.GetAsync("/health");
        response.StatusCode.Should().Be(HttpStatusCode.OK);
    }
}

Requires <InternalsVisibleTo Include="TestProject" /> or public partial class Program { } in the server project.

Autofac Integration

For projects using Autofac as the DI container:

var builder = WebApplication.CreateSlimBuilder(args);

builder.Host.UseServiceProviderFactory(new AutofacServiceProviderFactory());
builder.Host.ConfigureContainer<ContainerBuilder>(containerBuilder =>
{
    containerBuilder.RegisterModule<CoreAutofacModule>();
});

Embedded Kestrel Server

When embedding Kestrel in a CLI tool (like Recyclarr's HTTP server mode):

  • Use CreateSlimBuilder() for minimal footprint
  • Configure bind address and port via settings, not hardcoded
  • Add a health endpoint (/health) for readiness checks
  • Implement graceful shutdown via IHostApplicationLifetime or app.StopAsync()
  • Share the WebApplication startup logic between persistent (serve) and ephemeral modes
// Shared server startup
public static WebApplication BuildServer(string[] args, Action<ContainerBuilder>? configure = null)
{
    var builder = WebApplication.CreateSlimBuilder(args);

    builder.Host.UseServiceProviderFactory(new AutofacServiceProviderFactory());
    builder.Host.ConfigureContainer<ContainerBuilder>(cb =>
    {
        cb.RegisterModule<CoreAutofacModule>();
        configure?.Invoke(cb);
    });

    builder.Services.AddOpenApi();

    var app = builder.Build();

    app.MapOpenApi();
    app.MapHealthChecks("/health");
    app.MapSyncEndpoints();

    return app;
}

Key Microsoft Learn References

  • APIs overview
  • Minimal APIs quick reference
  • Parameter binding
  • Responses and TypedResults
  • Endpoint filters
  • OpenAPI document generation
  • Error handling in APIs
  • What's new in .NET 10
Repository
recyclarr/recyclarr
Last updated
First committed

Is this your skill?

If you maintain this skill, you can claim it as your own. Once claimed, you can manage eval scenarios, bundle related skills, attach documentation or rules, and ensure cross-agent compatibility.