RESTful APIs mit ASP.NET Core entwickeln

15 Min. Lesezeit9. Februar 2026Aktualisiert: 9. März 2026
ASP.NET Core APIREST API .NETWeb API tutorialASP.NET Core controllerAPI development C#.NET REST best practicesSwagger .NETAPI versioning

# RESTful APIs mit ASP.NET Core

ASP.NET Core ist eine der staerksten Grundlagen fuer produktionsreife REST-APIs. Die Kombination aus einer performanten HTTP-Pipeline, einer ausgereiften Middleware-Architektur und klaren Konventionen macht das Framework zur ersten Wahl fuer wartbare Dienste. In Produktions-APIs, die ich gebaut habe, hat sich das Framework unter hoher Last zuverlaessig bewaehrt — doch das Beste herauszuholen erfordert bewusste architektonische Entscheidungen von Anfang an.

Technische Basis

Projektstruktur und Controller-Design

Eine gut organisierte API beginnt mit einer schlanken Controller-Schicht. Controller sollten duenne Orchestratoren sein, die Geschaeftslogik an Services delegieren:

csharp
[ApiController]
[Route(class="code-string">"api/v{version:apiVersion}/[controller]")]
[Produces(class="code-string">"application/json")]
public class ProductsController : ControllerBase
{
    private readonly IProductService _productService;
    private readonly ILogger<ProductsController> _logger;

    public ProductsController(IProductService productService, ILogger<ProductsController> logger)
    {
        _productService = productService;
        _logger = logger;
    }

    [HttpGet]
    [ProducesResponseType(typeof(PagedResult<ProductDto>), StatusCodes.Status200OK)]
    public async Task<IActionResult> GetAll([FromQuery] ProductQueryParameters query)
    {
        var result = await _productService.GetAllAsync(query);
        return Ok(result);
    }

    [HttpGet(class="code-string">"{id:guid}")]
    [ProducesResponseType(typeof(ProductDto), StatusCodes.Status200OK)]
    [ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status404NotFound)]
    public async Task<IActionResult> GetById(Guid id)
    {
        var product = await _productService.GetByIdAsync(id);
        if (product is null)
            return NotFound();

        return Ok(product);
    }

    [HttpPost]
    [ProducesResponseType(typeof(ProductDto), StatusCodes.Status201Created)]
    [ProducesResponseType(typeof(ValidationProblemDetails), StatusCodes.Status400BadRequest)]
    public async Task<IActionResult> Create([FromBody] CreateProductRequest request)
    {
        var product = await _productService.CreateAsync(request);
        return CreatedAtAction(nameof(GetById), new { id = product.Id }, product);
    }

    [HttpPut(class="code-string">"{id:guid}")]
    [ProducesResponseType(StatusCodes.Status204NoContent)]
    [ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status404NotFound)]
    public async Task<IActionResult> Update(Guid id, [FromBody] UpdateProductRequest request)
    {
        await _productService.UpdateAsync(id, request);
        return NoContent();
    }

    [HttpDelete(class="code-string">"{id:guid}")]
    [ProducesResponseType(StatusCodes.Status204NoContent)]
    public async Task<IActionResult> Delete(Guid id)
    {
        await _productService.DeleteAsync(id);
        return NoContent();
    }
}

Model Binding und DTOs

Geben Sie niemals Ihre Domain-Entitaeten direkt nach aussen. Verwenden Sie dedizierte Request- und Response-Modelle, um Ihren API-Vertrag stabil zu halten:

csharp
public record CreateProductRequest(
    string Name,
    string Description,
    decimal Price,
    string Category
);

public record ProductDto(
    Guid Id,
    string Name,
    string Description,
    decimal Price,
    string Category,
    DateTime CreatedAt
);

public record ProductQueryParameters
{
    public int Page { get; init; } = class="code-number">1;
    public int PageSize { get; init; } = class="code-number">20;
    public string? SortBy { get; init; }
    public string? Search { get; init; }
}

Validierung mit FluentValidation

Data Annotations genuegen fuer einfache Faelle, aber FluentValidation zeigt seine wahre Staerke, wenn die Regeln komplex werden. In Produktions-APIs, die ich gebaut habe, hat FluentValidation unzaehlige Stunden beim Debuggen fehlerhafter Requests gespart:

csharp
public class CreateProductRequestValidator : AbstractValidator<CreateProductRequest>
{
    public CreateProductRequestValidator()
    {
        RuleFor(x => x.Name)
            .NotEmpty().WithMessage(class="code-string">"Der Produktname ist erforderlich.")
            .MaximumLength(class="code-number">200).WithMessage(class="code-string">"Der Produktname darf class="code-number">200 Zeichen nicht ueberschreiten.");

        RuleFor(x => x.Description)
            .MaximumLength(class="code-number">2000);

        RuleFor(x => x.Price)
            .GreaterThan(class="code-number">0).WithMessage(class="code-string">"Der Preis muss ein positiver Wert sein.")
            .PrecisionScale(class="code-number">10, class="code-number">2, ignoreTrailingZeros: true)
            .WithMessage(class="code-string">"Der Preis darf maximal class="code-number">2 Nachkommastellen haben.");

        RuleFor(x => x.Category)
            .NotEmpty()
            .Must(BeAValidCategory).WithMessage(class="code-string">"Ungueltige Kategorie.");
    }

    private bool BeAValidCategory(string category)
    {
        var validCategories = new[] { class="code-string">"Electronics", class="code-string">"Clothing", class="code-string">"Books", class="code-string">"Food" };
        return validCategories.Contains(category);
    }
}

Registrieren Sie FluentValidation in Ihrer `Program.cs`:

csharp
builder.Services.AddValidatorsFromAssemblyContaining<CreateProductRequestValidator>();
builder.Services.AddFluentValidationAutoValidation();

Globale Fehlerbehandlung mit ProblemDetails

Konsistente Fehlerantworten sind nicht verhandelbar. Nutzen Sie die eingebaute `ProblemDetails`-Infrastruktur zusammen mit einem globalen Exception-Handler:

csharp
builder.Services.AddProblemDetails(options =>
{
    options.CustomizeProblemDetails = context =>
    {
        context.ProblemDetails.Extensions[class="code-string">"traceId"] = context.HttpContext.TraceIdentifier;
    };
});

app.UseExceptionHandler(appError =>
{
    appError.Run(async context =>
    {
        context.Response.ContentType = class="code-string">"application/problem+json";
        var exceptionFeature = context.Features.Get<IExceptionHandlerFeature>();
        var exception = exceptionFeature?.Error;

        var problemDetails = exception switch
        {
            NotFoundException ex => new ProblemDetails
            {
                Status = StatusCodes.Status404NotFound,
                Title = class="code-string">"Ressource nicht gefunden",
                Detail = ex.Message,
                Type = class="code-string">"https:class="code-commentclass="code-string">">//tools.ietf.org/html/rfc9110#section-class="code-number">15.5.class="code-number">5"
            },
            ConflictException ex => new ProblemDetails
            {
                Status = StatusCodes.Status409Conflict,
                Title = class="code-string">"Konflikt",
                Detail = ex.Message,
                Type = class="code-string">"https:class="code-commentclass="code-string">">//tools.ietf.org/html/rfc9110#section-class="code-number">15.5.class="code-number">10"
            },
            _ => new ProblemDetails
            {
                Status = StatusCodes.Status500InternalServerError,
                Title = class="code-string">"Interner Serverfehler",
                Detail = class="code-string">"Ein unerwarteter Fehler ist aufgetreten.",
                Type = class="code-string">"https:class="code-commentclass="code-string">">//tools.ietf.org/html/rfc9110#section-class="code-number">15.6.class="code-number">1"
            }
        };

        context.Response.StatusCode = problemDetails.Status ?? class="code-number">500;
        await context.Response.WriteAsJsonAsync(problemDetails);
    });
});

Best Practices fuer API-Design

Ressourcen-Benennung

Verwenden Sie Pluralnomen fuer Ressourcen und halten Sie URLs vorhersehbar:

  • `GET /api/v1/products` — alle Produkte auflisten
  • `GET /api/v1/products/{id}` — ein einzelnes Produkt abrufen
  • `POST /api/v1/products` — neues Produkt erstellen
  • `PUT /api/v1/products/{id}` — vollstaendige Aktualisierung
  • `PATCH /api/v1/products/{id}` — teilweise Aktualisierung
  • `DELETE /api/v1/products/{id}` — Produkt loeschen
  • `GET /api/v1/products/{id}/reviews` — verschachtelte Unterressourcen
  • Vermeiden Sie Verben in URLs. Statt `/api/products/getAll` verwenden Sie einfach `GET /api/products`. Die HTTP-Methode kommuniziert die Aktion bereits.

    HTTP-Methoden und Statuscodes

    Setzen Sie Statuscodes bewusst ein. Fuer alles 200 zurueckzugeben verschenkt die Moeglichkeit, Absichten klar zu kommunizieren:

    | Operation | Erfolgscode | Bedeutung |

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

    | GET | 200 OK | Ressource gefunden und zurueckgegeben |

    | POST | 201 Created | Ressource erstellt, Location-Header einschliessen |

    | PUT | 204 No Content | Ressource erfolgreich ersetzt |

    | PATCH | 200 OK | Teilaktualisierung, aktualisierten Zustand zurueckgeben |

    | DELETE | 204 No Content | Ressource geloescht |

    Fuer Fehler sollten Sie sich an diese Codes halten:

  • **400** — Fehlerhafte Anfrage, Validierungsfehler
  • **401** — Nicht authentifiziert
  • **403** — Authentifiziert, aber nicht autorisiert
  • **404** — Ressource nicht gefunden
  • **409** — Konflikt (Duplikat, Versionskonflikt)
  • **422** — Semantisch ungueltige Anfrage
  • **429** — Rate-Limit erreicht
  • **500** — Serverfehler (niemals beabsichtigt)
  • Paginierung, Filterung und Sortierung

    Jeder Listenendpunkt sollte von Anfang an Paginierung unterstuetzen. Nachtraeglich einzubauen ist immer muehsamer:

    csharp
    public class PagedResult<T>
    {
        public IReadOnlyList<T> Items { get; init; } = [];
        public int TotalCount { get; init; }
        public int Page { get; init; }
        public int PageSize { get; init; }
        public bool HasNextPage => Page * PageSize < TotalCount;
        public bool HasPreviousPage => Page > class="code-number">1;
    }

    Versionierungsstrategie

    Planen Sie Versionierung ab dem ersten Release. In Produktions-APIs, die ich gebaut habe, hat das Ueberspringen der Versionierung stets zu schmerzhaften Breaking-Change-Migrationen gefuehrt. URL-basierte Versionierung ist der expliziteste und clientfreundlichste Ansatz:

    csharp
    builder.Services.AddApiVersioning(options =>
    {
        options.DefaultApiVersion = new ApiVersion(class="code-number">1, class="code-number">0);
        options.AssumeDefaultVersionWhenUnspecified = true;
        options.ReportApiVersions = true;
        options.ApiVersionReader = ApiVersionReader.Combine(
            new UrlSegmentApiVersionReader(),
            new HeaderApiVersionReader(class="code-string">"X-API-Version")
        );
    })
    .AddApiExplorer(options =>
    {
        options.GroupNameFormat = class="code-string">"class="code-string">'v'VVV";
        options.SubstituteApiVersionInUrl = true;
    });

    Wenn ein Breaking Change noetig ist, erstellen Sie eine neue Controller-Version, waehrend die alte weiterhin aktiv bleibt:

    csharp
    [ApiController]
    [Route(class="code-string">"api/v{version:apiVersion}/products")]
    [ApiVersion(class="code-string">"class="code-number">2.0")]
    public class ProductsV2Controller : ControllerBase
    {
        [HttpGet(class="code-string">"{id:guid}")]
        public async Task<IActionResult> GetById(Guid id)
        {
            class=class="code-string">"code-comment">// V2 liefert eine reichhaltigere Antwortstruktur
            var product = await _productService.GetByIdDetailedAsync(id);
            return Ok(product);
        }
    }

    Markieren Sie veraltete Versionen explizit, damit Nutzer Zeit zur Migration haben:

    csharp
    [ApiVersion(class="code-string">"class="code-number">1.0", Deprecated = true)]

    OpenAPI/Swagger-Dokumentation

    Gute API-Dokumentation eliminiert die meisten Support-Anfragen. Richten Sie Swagger mit aussagekraeftigen Metadaten ein:

    csharp
    builder.Services.AddEndpointsApiExplorer();
    builder.Services.AddSwaggerGen(options =>
    {
        options.SwaggerDoc(class="code-string">"v1", new OpenApiInfo
        {
            Title = class="code-string">"Produktkatalog-API",
            Version = class="code-string">"v1",
            Description = class="code-string">"API zur Verwaltung des Produktkatalogs.",
            Contact = new OpenApiContact
            {
                Name = class="code-string">"API-Support",
                Email = class="code-string">"api-support@beispiel.de"
            }
        });
    
        options.SwaggerDoc(class="code-string">"v2", new OpenApiInfo
        {
            Title = class="code-string">"Produktkatalog-API",
            Version = class="code-string">"v2"
        });
    
        class=class="code-string">"code-comment">// XML-Kommentare aus dem Projekt einbinden
        var xmlFile = $class="code-string">"{Assembly.GetExecutingAssembly().GetName().Name}.xml";
        var xmlPath = Path.Combine(AppContext.BaseDirectory, xmlFile);
        options.IncludeXmlComments(xmlPath);
    
        class=class="code-string">"code-comment">// JWT-Authentifizierung fuer Swagger UI hinzufuegen
        options.AddSecurityDefinition(class="code-string">"Bearer", new OpenApiSecurityScheme
        {
            Description = class="code-string">"JWT-Autorisierung ueber den Bearer-Header.",
            Name = class="code-string">"Authorization",
            In = ParameterLocation.Header,
            Type = SecuritySchemeType.Http,
            Scheme = class="code-string">"bearer"
        });
    
        options.AddSecurityRequirement(new OpenApiSecurityRequirement
        {
            {
                new OpenApiSecurityScheme
                {
                    Reference = new OpenApiReference
                    {
                        Type = ReferenceType.SecurityScheme,
                        Id = class="code-string">"Bearer"
                    }
                },
                Array.Empty<string>()
            }
        });
    });

    Aktivieren Sie die Swagger-Middleware:

    csharp
    if (app.Environment.IsDevelopment())
    {
        app.UseSwagger();
        app.UseSwaggerUI(options =>
        {
            options.SwaggerEndpoint(class="code-string">"/swagger/v1/swagger.json", class="code-string">"Produkt-API V1");
            options.SwaggerEndpoint(class="code-string">"/swagger/v2/swagger.json", class="code-string">"Produkt-API V2");
        });
    }

    Best Practices fuer die Produktion

  • **Idempotenz-Schluessel** fuer kritische Schreibendpunkte — verwenden Sie einen `Idempotency-Key`-Header, um fehlgeschlagene POST-Anfragen sicher wiederholen zu koennen
  • **Korrelations-IDs** fuer verteiltes Tracing — propagieren Sie einen `X-Correlation-Id`-Header ueber Servicegrenzen hinweg
  • **Health Checks** fuer Orchestratoren — stellen Sie `/health/live`- und `/health/ready`-Endpunkte bereit
  • **Rate Limiting** zum Schutz Ihres Dienstes — nutzen Sie die eingebaute `RateLimiter`-Middleware ab .NET 7
  • **Output Caching** fuer teure, leseanfaellige Endpunkte
  • Performance-Tipps

  • Setzen Sie `async`/`await` durchgaengig ein — ein einzelner synchroner Aufruf in der Kette blockiert einen Thread-Pool-Thread
  • Verwenden Sie `IAsyncEnumerable<T>` fuer das Streaming grosser Datensaetze, anstatt alles im Speicher zu puffern
  • Wenden Sie Response-Komprimierung mit `UseResponseCompression()` fuer JSON-lastige Payloads an
  • Profilieren Sie mit `dotnet-trace` und `dotnet-counters`, bevor Sie mikrooptimieren
  • Nutzen Sie `System.Text.Json`-Source-Generatoren fuer AOT-kompatible Serialisierung
  • Sicherheitsgrundlagen

  • Erzwingen Sie HTTPS ueberall mit `UseHttpsRedirection()` und HSTS-Headern
  • Verwenden Sie JWT/OAuth2 fuer zustandslose Authentifizierung mit kurzlebigen Access-Tokens
  • Setzen Sie Policy-basierte Autorisierung fuer feinkoernige Zugriffskontrolle ein
  • Loggen Sie niemals sensible Daten — maskieren Sie PII und Geheimnisse in strukturierten Logs
  • Validieren und bereinigen Sie alle Benutzereingaben, auch in authentifizierten Endpunkten
  • Haeufige API-Design-Fehler

    Fuer alles 200 zurueckgeben. Wenn Ihre API `200 OK` mit `{ "success": false, "error": "Nicht gefunden" }` zurueckgibt, arbeiten Sie gegen HTTP, anstatt es zu nutzen. Clients koennen sich nicht auf Statuscodes verlassen, Middleware wie Caches und Load Balancer werden verwirrt, und Retry-Logik funktioniert nicht mehr.

    Domain-Entitaeten als Antworten exponieren. Sobald Ihr Datenbankschema in den API-Vertrag durchsickert, wird jede Schema-Migration zu einem Breaking Change. Verwenden Sie immer dedizierte DTOs.

    Paginierung bei Listenendpunkten ignorieren. Mit 50 Datensaetzen in der Entwicklung funktioniert alles einwandfrei. Dann erreicht die Produktion 500.000 Datensaetze und Ihr Endpunkt beginnt mit Timeouts. Paginieren Sie von Anfang an.

    Keine Versionierung von Beginn an. "Wir fuegen Versionierung hinzu, wenn wir sie brauchen" bedeutet, dass Sie eine Notfallmigration durchfuehren werden, wenn ein Breaking Change unvermeidlich ist. URL-basierte Versionierung kostet praktisch nichts in der Einrichtung.

    Inkonsistente Fehlerformate. Wenn manche Endpunkte `{ "error": "Nachricht" }` zurueckgeben und andere `ProblemDetails`, muss jeder Client mehrere Formate verarbeiten. Waehlen Sie einen Standard und setzen Sie ihn global durch.

    Request-Validierung ueberspringen. Darauf zu vertrauen, dass Clients wohlgeformte Daten senden, ist ein Rezept fuer korrumpierten Zustand und kryptische 500er-Fehler. Validieren Sie immer an der Grenze.

    Fazit

    ASP.NET Core bietet die Struktur und Performance, die fuer zuverlaessige Enterprise-APIs benoetigt werden, aber Qualitaet entsteht durch klare Vertraege, starke Observability und disziplinierte Release-Praktiken. In Produktions-APIs, die ich gebaut habe, lag der Unterschied zwischen einer guten und einer grossartigen API immer in den Details: konsistente Fehlerbehandlung, durchdachte Versionierung und Dokumentation, die Entwickler tatsaechlich lesen moechten.

    Ich unterstuetze Sie gerne beim Entwurf und Hardening Ihrer API-Architektur fuer die Produktion.

    Verwandte Artikel

    Haben Sie ein Flutter-Projekt?

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

    Kontakt aufnehmen