Building Clean APIs with ASP.NET Core
Best practices for designing maintainable, scalable, and secure REST APIs
Creating an API that works is easy. Creating an API that is maintainable, testable, and pleasant to consume is an art. In the .NET ecosystem, ASP.NET Core combined with Entity Framework (EF) Core gives us a powerful toolkit, but it's up to us to use it wisely.
This guide covers the essential patterns for building clean, professional-grade APIs, moving beyond simple CRUD to robust enterprise architecture.
1. Separation of Concerns: The Service Layer
One of the most common mistakes is putting business logic directly inside Controllers. This makes code hard to test and reuse. Instead, aim for a clear layered architecture.
The Onion Architecture: Dependencies point inwards.
❌ The Bad Way (Fat Controller)
[HttpPost]
public IActionResult CreateOrder(OrderDto dto) {
// Validation logic mixed with business logic
if(dto.Amount < 0) return BadRequest();
var order = new Order { ... };
_dbContext.Orders.Add(order); // Direct DB access in controller
_dbContext.SaveChanges();
return Ok(order);
}
✅ The Clean Way (Service Pattern)
[HttpPost]
public async Task<IActionResult> CreateOrder(OrderDto dto) {
// Controller only handles HTTP concerns
var result = await _orderService.CreateOrderAsync(dto);
return Ok(result);
}
2. Global Error Handling with Middleware
Don't wrap every controller action in a try-catch block. It clutters your code. Instead, use
Middleware to catch exceptions globally and return a standardized error response.
The Request Pipeline: Logic flows through middleware layers.
public class ExceptionMiddleware
{
private readonly RequestDelegate _next;
public ExceptionMiddleware(RequestDelegate next)
{
_next = next;
}
public async Task InvokeAsync(HttpContext httpContext)
{
try
{
await _next(httpContext);
}
catch (Exception ex)
{
await HandleExceptionAsync(httpContext, ex);
}
}
private Task HandleExceptionAsync(HttpContext context, Exception exception)
{
context.Response.ContentType = "application/json";
context.Response.StatusCode = (int)HttpStatusCode.InternalServerError;
return context.Response.WriteAsync(new ErrorDetails()
{
StatusCode = context.Response.StatusCode,
Message = "Internal Server Error from the custom middleware."
}.ToString());
}
}
3. Validation with FluentValidation
Data annotations (Attributes) can get messy. FluentValidation allows you to separate validation rules from your models, keeping your DTOs clean.
public class OrderValidator : AbstractValidator<OrderDto>
{
public OrderValidator()
{
RuleFor(x => x.ProductName).NotEmpty();
RuleFor(x => x.Amount).GreaterThan(0).WithMessage("Price must be positive");
}
}
4. Use DTOs (Data Transfer Objects)
Never expose your database entities directly to the API client. It creates a tight coupling between your database schema and your public interface.
Scenario: You add a `PasswordHash` column to your User table. If you return the Entity directly, you just leaked everyone's password hashes!
Always map your Entites to DTOs. Libraries like AutoMapper or manual mapping work great.
Conclusion
Building clean APIs is about discipline. By separating concerns, implementing global error handling, and using DTOs, you create a codebase that your team will enjoy working on for years to come.
No comments:
Post a Comment