RESTful APIs mit ASP.NET Core entwickeln
# 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:
[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:
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:
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`:
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:
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:
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:
Paginierung, Filterung und Sortierung
Jeder Listenendpunkt sollte von Anfang an Paginierung unterstuetzen. Nachtraeglich einzubauen ist immer muehsamer:
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:
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:
[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:
[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:
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:
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
Performance-Tipps
Sicherheitsgrundlagen
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
Was ist .NET? Ein moderner Backend-Entwicklungsleitfaden
Erfahren Sie, was .NET ist, wie es funktioniert und warum es für Enterprise-Backend gewählt wird.
Entity Framework Core: Der moderne Weg für Datenbankoperationen
Verwalten Sie Datenbankoperationen mit Entity Framework Core. Code-First, Migrations und Performance.
.NET Minimal APIs: Leichtgewichtige und schnelle Endpoints
Erstellen Sie schnelle Endpoints mit .NET Minimal APIs. Controller-freier Ansatz und Anwendungsfälle.
Haben Sie ein Flutter-Projekt?
Ich entwickle hochleistungsfähige Flutter-Anwendungen für iOS, Android und Web.
Kontakt aufnehmen