.NET Minimal APIs: Leichtgewichtige und schnelle Endpoints

13 Min. Lesezeit9. Februar 2026Aktualisiert: 9. März 2026
.NET Minimal APIMinimal APIs tutorial.NET 8 minimal apiController vs Minimal APILightweight .NET APIFast .NET endpoints.NET microservicesServerless .NET

# .NET Minimal APIs: Schlanke und schnelle Endpunkte

Minimal APIs wurden mit .NET 6 als radikal vereinfachter Weg eingefuhrt, HTTP-Endpunkte in ASP.NET Core zu erstellen. Statt Controller-Klassen, Attributen und zeremoniellen Klassenhierarchien definieren Sie Routen direkt mit Lambda-Ausdrucken. Unter dieser Einfachheit steht Ihnen jedoch die gesamte ASP.NET-Core-Plattform zur Verfugung: Dependency Injection, Middleware, Authentifizierung, Autorisierung und OpenAPI-Tooling funktionieren genau wie erwartet.

In Microservices, die ich mit Minimal APIs gebaut habe, sank die Startzeit spuerbar, und der kognitive Aufwand fuer neue Teammitglieder wurde deutlich geringer. Ein Entwickler kann `Program.cs` oeffnen, jede Route im Service sehen und innerhalb von Minuten produktiv mitarbeiten.

Wo Minimal APIs glaenzen

  • **Fokussierte Microservices** mit begrenzter Endpunkt-Oberflaeche (5-20 Endpunkte)
  • **Interne Tools und BFF-Services**, die Daten fuer Frontends aggregieren
  • **MVPs und Prototypen**, die schnelle Iteration ohne Scaffolding-Overhead benoetigen
  • **Einfache CRUD- und Webhook-Handler**, bei denen Controller-Zeremonie keinen Mehrwert bietet
  • **Serverless Functions** und containerbasierte Workloads, bei denen Kaltstartzeit entscheidend ist
  • **Event-driven Services**, die Nachrichten empfangen und Health-/Status-Endpunkte bereitstellen
  • Grundlagen: Der Einstieg

    Eine vollstaendige Minimal API kann in einer einzigen Datei leben:

    csharp
    var builder = WebApplication.CreateBuilder(args);
    builder.Services.AddSingleton<IProductRepository, ProductRepository>();
    
    var app = builder.Build();
    
    app.MapGet(class="code-string">"/products", async (IProductRepository repo) =>
    {
        var products = await repo.GetAllAsync();
        return Results.Ok(products);
    });
    
    app.MapGet(class="code-string">"/products/{id:int}", async (int id, IProductRepository repo) =>
    {
        var product = await repo.GetByIdAsync(id);
        return product is not null ? Results.Ok(product) : Results.NotFound();
    });
    
    app.MapPost(class="code-string">"/products", async (CreateProductRequest request, IProductRepository repo) =>
    {
        var product = await repo.CreateAsync(request);
        return Results.Created($class="code-string">"/products/{product.Id}", product);
    });
    
    app.Run();

    Beachten Sie, wie Dependency Injection ueber Parameter-Binding funktioniert. Das Framework untersucht die Handler-Parameter und loest Services, Route-Werte, Query-Strings und Body-Inhalte automatisch auf.

    Parameter-Binding im Detail

    Minimal APIs verwenden ein konventionsbasiertes Binding-System, das sowohl maechtig als auch intuitiv ist:

    csharp
    class=class="code-string">"code-comment">// Route-Parameter
    app.MapGet(class="code-string">"/users/{id:guid}", (Guid id) => ...);
    
    class=class="code-string">"code-comment">// Query-String-Parameter
    app.MapGet(class="code-string">"/search", (string? query, int page = class="code-number">1, int pageSize = class="code-number">20) => ...);
    
    class=class="code-string">"code-comment">// Header-Binding
    app.MapGet(class="code-string">"/protected", ([FromHeader(Name = class="code-string">"X-Api-Key")] string apiKey) => ...);
    
    class=class="code-string">"code-comment">// Body-Binding (automatisch fuer komplexe Typen bei POST/PUT)
    app.MapPost(class="code-string">"/orders", (CreateOrderRequest request) => ...);
    
    class=class="code-string">"code-comment">// Service-Injection
    app.MapGet(class="code-string">"/stats", (IAnalyticsService analytics, ILogger<Program> logger) => ...);
    
    class=class="code-string">"code-comment">// HttpContext-Zugriff bei Bedarf an voller Kontrolle
    app.MapGet(class="code-string">"/custom", (HttpContext context) => ...);
    
    class=class="code-string">"code-comment">// AsParameters zum Gruppieren mehrerer Bindings
    app.MapGet(class="code-string">"/products", async ([AsParameters] ProductFilterRequest filter) => ...);

    Das `[AsParameters]`-Attribut ist besonders nuetzlich fuer GET-Endpunkte mit vielen Query-Parametern:

    csharp
    public record ProductFilterRequest(
        string? Category,
        decimal? MinPrice,
        decimal? MaxPrice,
        int Page = class="code-number">1,
        int PageSize = class="code-number">20,
        [FromServices] IProductRepository Repository = default!
    );

    Typed Results fuer klare Vertraege

    Eine der groessten Verbesserungen seit .NET 7 ist `TypedResults`, das Ihre Endpunkt-Vertraege explizit macht und direkt in die OpenAPI-Dokumentation einfliesst:

    csharp
    app.MapGet(class="code-string">"/orders/{id}", async Task<Results<Ok<Order>, NotFound>> (int id, IOrderService service) =>
    {
        var order = await service.GetByIdAsync(id);
        return order is not null
            ? TypedResults.Ok(order)
            : TypedResults.NotFound();
    });
    
    app.MapPost(class="code-string">"/orders", async Task<Results<Created<Order>, ValidationProblem>> (
        CreateOrderRequest request, IOrderService service, IValidator<CreateOrderRequest> validator) =>
    {
        var validation = await validator.ValidateAsync(request);
        if (!validation.IsValid)
            return TypedResults.ValidationProblem(validation.ToDictionary());
    
        var order = await service.CreateAsync(request);
        return TypedResults.Created($class="code-string">"/orders/{order.Id}", order);
    });

    Der `Results`-Union-Typ teilt sowohl dem Compiler als auch dem OpenAPI-Generator genau mit, welche Antworten ein Endpunkt erzeugen kann.

    Route Groups fuer Feature-basierte Organisation

    Route Groups ermoeglichen es, Konfiguration zwischen verwandten Endpunkten zu teilen und so Wiederholungen zu reduzieren:

    csharp
    var api = app.MapGroup(class="code-string">"/api/v1");
    
    var products = api.MapGroup(class="code-string">"/products")
        .WithTags(class="code-string">"Products")
        .RequireAuthorization(class="code-string">"ApiScope");
    
    products.MapGet(class="code-string">"/", GetAllProducts);
    products.MapGet(class="code-string">"/{id:int}", GetProductById);
    products.MapPost(class="code-string">"/", CreateProduct).RequireAuthorization(class="code-string">"Admin");
    products.MapPut(class="code-string">"/{id:int}", UpdateProduct).RequireAuthorization(class="code-string">"Admin");
    products.MapDelete(class="code-string">"/{id:int}", DeleteProduct).RequireAuthorization(class="code-string">"Admin");
    
    var orders = api.MapGroup(class="code-string">"/orders")
        .WithTags(class="code-string">"Orders")
        .RequireAuthorization();
    
    orders.MapGet(class="code-string">"/", GetUserOrders);
    orders.MapGet(class="code-string">"/{id:int}", GetOrderById);
    orders.MapPost(class="code-string">"/", PlaceOrder);

    Groups unterstuetzen verschachtelte Komposition, gemeinsame Filter und Autorisierungsrichtlinien. Sie sind der primaere Baustein zur Organisation von Minimal APIs jenseits trivialer Beispiele.

    Endpoint Filters: Querschnittslogik

    Endpoint-Filter sind das Minimal-API-Aequivalent zu Action-Filtern in MVC. Sie fangen Anfragen vor und nach der Handler-Ausfuehrung ab:

    csharp
    public class ValidationFilter<T> : IEndpointFilter where T : class
    {
        public async ValueTask<object?> InvokeAsync(
            EndpointFilterInvocationContext context,
            EndpointFilterDelegate next)
        {
            var request = context.Arguments.OfType<T>().FirstOrDefault();
            if (request is null)
                return TypedResults.BadRequest(class="code-string">"Request-Body ist erforderlich.");
    
            var validator = context.HttpContext.RequestServices.GetService<IValidator<T>>();
            if (validator is not null)
            {
                var result = await validator.ValidateAsync(request);
                if (!result.IsValid)
                    return TypedResults.ValidationProblem(result.ToDictionary());
            }
    
            return await next(context);
        }
    }
    
    class=class="code-string">"code-comment">// Verwendung
    products.MapPost(class="code-string">"/", CreateProduct)
        .AddEndpointFilter<ValidationFilter<CreateProductRequest>>();

    Fuer einfachere Szenarien koennen Sie auch Inline-Filter definieren:

    csharp
    app.MapGet(class="code-string">"/items/{id:int}", (int id) => ...)
        .AddEndpointFilter(async (context, next) =>
        {
            var id = context.GetArgument<int>(class="code-number">0);
            if (id <= class="code-number">0)
                return TypedResults.BadRequest(class="code-string">"ID muss positiv sein.");
            return await next(context);
        });

    OpenAPI-Integration

    Seit .NET 9 verfuegen Minimal APIs ueber erstklassige, ins Framework integrierte OpenAPI-Unterstuetzung:

    csharp
    builder.Services.AddOpenApi();
    
    var app = builder.Build();
    
    app.MapOpenApi();
    
    app.MapGet(class="code-string">"/products/{id}", async (int id, IProductRepository repo) =>
    {
        var product = await repo.GetByIdAsync(id);
        return product is not null ? Results.Ok(product) : Results.NotFound();
    })
    .WithName(class="code-string">"GetProductById")
    .WithDescription(class="code-string">"Ruft ein einzelnes Produkt anhand seiner eindeutigen Kennung ab.")
    .Produces<Product>(class="code-number">200)
    .Produces(class="code-number">404)
    .WithTags(class="code-string">"Products");

    Fuer aeltere .NET-Versionen integrieren sich Swashbuckle oder NSwag nahtlos. Entscheidend ist, dass `TypedResults` automatisch korrekte Schemas generiert, sodass Sie ohne manuelle Annotationen zuverlaessige Dokumentation erhalten.

    Minimal APIs im grossen Massstab strukturieren

    Der Einzeldatei-Ansatz funktioniert fuer kleine Services, aber alles mit mehr als einer Handvoll Endpunkten braucht Struktur. Das Pattern, das sich in meiner Erfahrung am besten bewaehrt hat, ist die Organisation von Endpunkten in feature-basierten statischen Klassen:

    csharp
    class=class="code-string">"code-comment">// Features/Products/ProductEndpoints.cs
    public static class ProductEndpoints
    {
        public static RouteGroupBuilder MapProductEndpoints(this IEndpointRouteBuilder routes)
        {
            var group = routes.MapGroup(class="code-string">"/products").WithTags(class="code-string">"Products");
    
            group.MapGet(class="code-string">"/", GetAll);
            group.MapGet(class="code-string">"/{id:int}", GetById);
            group.MapPost(class="code-string">"/", Create).RequireAuthorization(class="code-string">"Admin");
            group.MapPut(class="code-string">"/{id:int}", Update).RequireAuthorization(class="code-string">"Admin");
            group.MapDelete(class="code-string">"/{id:int}", Delete).RequireAuthorization(class="code-string">"Admin");
    
            return group;
        }
    
        private static async Task<Ok<List<ProductDto>>> GetAll(
            IProductRepository repo, CancellationToken ct)
        {
            var products = await repo.GetAllAsync(ct);
            return TypedResults.Ok(products);
        }
    
        private static async Task<Results<Ok<ProductDto>, NotFound>> GetById(
            int id, IProductRepository repo, CancellationToken ct)
        {
            var product = await repo.GetByIdAsync(id, ct);
            return product is not null
                ? TypedResults.Ok(product)
                : TypedResults.NotFound();
        }
    
        class=class="code-string">"code-comment">// ... weitere Handler
    }
    
    class=class="code-string">"code-comment">// Program.cs bleibt sauber
    var app = builder.Build();
    app.MapProductEndpoints();
    app.MapOrderEndpoints();
    app.MapUserEndpoints();
    app.Run();

    Dieser Ansatz bietet die Klarheit, alle Routen pro Feature an einem Ort zu sehen, waehrend `Program.cs` als Composition Root sauber bleibt. Jede Feature-Datei besitzt ihre eigenen Routen, Handler und Autorisierungsanforderungen.

    Auth, Validierung und Fehlerbehandlung

    Authentifizierung und Autorisierung

    csharp
    builder.Services.AddAuthentication().AddJwtBearer();
    builder.Services.AddAuthorizationBuilder()
        .AddPolicy(class="code-string">"Admin", policy => policy.RequireRole(class="code-string">"admin"))
        .AddPolicy(class="code-string">"ApiScope", policy => policy.RequireClaim(class="code-string">"scope", class="code-string">"api"));
    
    var app = builder.Build();
    
    class=class="code-string">"code-comment">// Global auf eine Route Group anwenden
    var api = app.MapGroup(class="code-string">"/api").RequireAuthorization(class="code-string">"ApiScope");
    
    class=class="code-string">"code-comment">// Fuer bestimmte Endpunkte ueberschreiben
    api.MapGet(class="code-string">"/public/health", () => Results.Ok(class="code-string">"Healthy")).AllowAnonymous();
    api.MapDelete(class="code-string">"/admin/cache", (ICacheService cache) => cache.Clear()).RequireAuthorization(class="code-string">"Admin");

    Zentrale Validierung mit FluentValidation

    csharp
    class=class="code-string">"code-comment">// Alle Validatoren aus der Assembly registrieren
    builder.Services.AddValidatorsFromAssemblyContaining<Program>();
    
    class=class="code-string">"code-comment">// Generischer Validierungsfilter pro Route Group
    public static RouteGroupBuilder WithValidation<T>(this RouteGroupBuilder group) where T : class
    {
        return group.AddEndpointFilter<ValidationFilter<T>>();
    }

    Globale Fehlerbehandlung

    csharp
    app.UseExceptionHandler(errorApp =>
    {
        errorApp.Run(async context =>
        {
            var exception = context.Features.Get<IExceptionHandlerFeature>()?.Error;
            var logger = context.RequestServices.GetRequiredService<ILogger<Program>>();
            logger.LogError(exception, class="code-string">"Unbehandelte Ausnahme");
    
            context.Response.StatusCode = exception switch
            {
                NotFoundException => class="code-number">404,
                ValidationException => class="code-number">400,
                UnauthorizedAccessException => class="code-number">403,
                _ => class="code-number">500
            };
    
            await context.Response.WriteAsJsonAsync(new ProblemDetails
            {
                Status = context.Response.StatusCode,
                Title = exception switch
                {
                    NotFoundException => class="code-string">"Ressource nicht gefunden",
                    ValidationException => class="code-string">"Validierung fehlgeschlagen",
                    _ => class="code-string">"Ein Fehler ist aufgetreten"
                }
            });
        });
    });

    In Microservices, die ich mit Minimal APIs gebaut habe, deckt die Kombination aus globalem Exception-Handler und Endpoint-Filtern fuer die Validierung die grosse Mehrheit der Fehlerszenarien sauber ab. Der Handler faengt Unerwartetes auf, und die Filter behandeln die vorhersehbaren Faelle nah am Endpunkt.

    Minimal APIs vs. Controller: Entscheidungsrahmen

    | Kriterium | Minimal APIs | Controller |

    |---|---|---|

    | Boilerplate | Sehr gering, Lambda-basiert | Moderat, Klasse + Attribute |

    | Startperformance | Schneller, kein Reflection-Overhead | Etwas langsamer |

    | Auffindbarkeit | Alle Routen in einer Datei oder Feature-Modul sichtbar | Ueber Controller-Klassen verteilt |

    | Model-Binding | Konventionsbasiert, einfacher | Umfangreicher, konfigurierbarer |

    | Filter/Middleware | Endpoint-Filter (neuer, einfacher) | Action-Filter (ausgereift, voll ausgestattet) |

    | OpenAPI-Support | Eingebaut mit TypedResults | Attribut-getrieben, etabliert |

    | Testbarkeit | Direkte Funktionsaufrufe | Controller-Instanziierung erforderlich |

    | Team-Vertrautheit | Neueres Paradigma | Bekanntes MVC-Pattern |

    | Grosse API-Oberflaechen | Erfordert Disziplin bei der Organisation | Natuerlich durch Klassen strukturiert |

    | Content Negotiation | Manuelles Setup | Eingebaut |

    Waehlen Sie Minimal APIs, wenn: Sie fokussierte Microservices, BFFs oder jeden Service bauen, bei dem Einfachheit und Startgeschwindigkeit wichtiger sind als MVC-Konventionen. Wenn Ihr Service weniger als 30-40 Endpunkte hat, werden Minimal APIs wahrscheinlich die sauberere Loesung sein.

    Waehlen Sie Controller, wenn: Sie eine grosse API-Oberflaeche mit komplexem Model-Binding, Content Negotiation oder ein Team haben, das tief in MVC-Patterns investiert ist. An Controllern ist nichts auszusetzen -- sie sind kampferprobt und gut verstanden.

    Der hybride Ansatz funktioniert ebenfalls: Verwenden Sie Minimal APIs fuer einfache CRUD- und Webhook-Endpunkte, waehrend Sie Controller fuer komplexe Bereiche beibehalten, die von MVC-Konventionen profitieren. ASP.NET Core unterstuetzt beides im selben Projekt.

    Haeufige Minimal-API-Fehler

    1. Geschaeftslogik in Handlern platzieren

    csharp
    class=class="code-string">"code-comment">// Schlecht: Handler macht alles
    app.MapPost(class="code-string">"/orders", async (CreateOrderRequest req, AppDbContext db) =>
    {
        class=class="code-string">"code-comment">// class="code-number">30 Zeilen Validierung, Geschaeftsregeln und Persistenz
        class=class="code-string">"code-comment">// Das wird schnell unwartbar
    });
    
    class=class="code-string">"code-comment">// Gut: Handler delegiert an einen Service
    app.MapPost(class="code-string">"/orders", async (CreateOrderRequest req, IOrderService service) =>
    {
        var result = await service.PlaceOrderAsync(req);
        return result.IsSuccess
            ? TypedResults.Created($class="code-string">"/orders/{result.Value.Id}", result.Value)
            : TypedResults.BadRequest(result.Error);
    });

    2. CancellationToken ignorieren

    csharp
    class=class="code-string">"code-comment">// Schlecht: keine Abbruch-Unterstuetzung
    app.MapGet(class="code-string">"/reports", async (IReportService service) =>
        await service.GenerateAsync());
    
    class=class="code-string">"code-comment">// Gut: Abbruch weitergeben
    app.MapGet(class="code-string">"/reports", async (IReportService service, CancellationToken ct) =>
        await service.GenerateAsync(ct));

    3. Route Groups nicht verwenden

    Jede Route auf oberster Ebene mit wiederholten `.RequireAuthorization()`- und `.WithTags()`-Aufrufen zu definieren, fuehrt zu laermigem, schwer wartbarem Code. Route Groups existieren genau zur Loesung dieses Problems.

    4. Fuer alles `Results.Ok()` verwenden

    csharp
    class=class="code-string">"code-comment">// Schlecht: gibt immer class="code-number">200 zurueck, auch bei Erstellung
    app.MapPost(class="code-string">"/items", async (Item item, IItemService service) =>
        Results.Ok(await service.CreateAsync(item)));
    
    class=class="code-string">"code-comment">// Gut: semantisch korrekte Statuscodes verwenden
    app.MapPost(class="code-string">"/items", async (Item item, IItemService service) =>
    {
        var created = await service.CreateAsync(item);
        return TypedResults.Created($class="code-string">"/items/{created.Id}", created);
    });

    5. Strukturiertes Logging ueberspringen

    Minimal APIs haben kein eingebautes Action-Logging wie MVC. Fuegen Sie Request-/Response-Logging explizit ueber Middleware oder Endpoint-Filter hinzu, sonst debuggen Sie in der Produktion blind.

    Fazit

    Minimal APIs sind kein Spielzeug und keine Abkuerzung -- sie sind ein ausgereifter, produktionsreifer Ansatz zum Erstellen von HTTP-Services in .NET. Der Unterschied zu Controllern liegt in der Syntax und der Zeremonie, nicht in den Engineering-Standards. Saubere Architektur, ordentliche Validierung, strukturierte Fehlerbehandlung und durchdachte Endpunkt-Organisation bleiben unverzichtbar.

    Die zentrale Erkenntnis ist, dass Minimal APIs Reibung beseitigen, ohne Faehigkeiten zu reduzieren. Jedes Feature, auf das Sie im Controller-basierten ASP.NET Core vertrauen -- DI, Auth, OpenAPI, Filter und Middleware -- ist vollstaendig verfuegbar. Was Sie gewinnen, ist Klarheit: Die gesamte Oberflaeche Ihres Services ist sichtbar, komponierbar und leicht nachvollziehbar.

    Ich helfe gern bei der Entscheidung zwischen Minimal APIs und Controller-Ansatz und beim Entwurf einer Endpunkt-Struktur, die sauber skaliert.

    Verwandte Artikel

    Haben Sie ein Flutter-Projekt?

    Ich entwickle hochleistungsfähige Flutter-Anwendungen für iOS, Android und Web.

    Kontakt aufnehmen