ASP.NET Core Best Practices for Production APIs
A focused checklist for designing observable, resilient, and maintainable ASP.NET Core APIs in production.
Production quality is less about a clever endpoint and more about predictable behavior under failure. A good API makes its contracts explicit, stops work when clients disconnect, and leaves enough evidence to diagnose the unexpected.
Keep transport and domain concerns separate
Controllers or route handlers should translate HTTP input into an application request. Business decisions belong in services or use cases that can be tested without constructing an HTTP context.
app.MapPost("/orders", async (
CreateOrderRequest request,
IOrderService orders,
CancellationToken cancellationToken) =>
{
var result = await orders.CreateAsync(request, cancellationToken);
return Results.Created($"/orders/{result.Id}", result);
});
The handler owns status codes and locations. The service owns the rules for creating an order.
Validate at the boundary
Reject invalid input before it reaches domain logic. Use consistent problem details so clients can handle every validation failure through the same shape.
Return useful errors without leaking internals
An exception stack trace helps your logs, not your API consumer. Map known failures to stable problem types and keep unexpected details on the server.
| Failure | Typical response |
|---|---|
| Invalid request | 400 Bad Request |
| Missing resource | 404 Not Found |
| Version conflict | 409 Conflict |
| Unexpected failure | 500 Internal Server Error |
Propagate cancellation tokens
Pass the request cancellation token through every asynchronous boundary, including database and outbound HTTP calls. This prevents abandoned requests from continuing expensive work.
var order = await db.Orders
.AsNoTracking()
.SingleOrDefaultAsync(x => x.Id == id, cancellationToken);
Make observability part of the design
Structured logs should identify the request and the business operation without recording secrets. Add health checks for real dependencies, emit useful metrics, and propagate trace context across service calls.
If a failure cannot be connected to a request, dependency, and deployment, it will take longer to fix than it should.
Test the contract
Unit tests protect domain rules. Integration tests protect routing, serialization, authentication, and database behavior. Both matter, but production incidents often hide in the seams that integration tests exercise.
A production API is trustworthy when its behavior stays clear under both normal load and partial failure.