Summary
One of the most important lessons I’ve learned while building APIs in ASP.NET Core is the value of clean architecture. Without structure, APIs become hard to maintain, test, and extend. In this post, we’ll look at how to properly separate concerns using Controllers, Services, and Repositories.
⚡ Challenge
When starting out, it’s easy to put all the logic directly in your controllers. This quickly leads to problems:
- Controllers become too large and unmanageable.
- Business logic is duplicated and hard to test.
- Database queries are mixed with request handling.
Solution
The best practice is to use a layered architecture:
- Controller → Handles HTTP requests/responses.
- Service Layer → Contains business logic.
- Repository Layer → Handles database operations.
This separation makes your API more maintainable, testable, and easier to scale.
Code Example
Controller
[ApiController]
[Route("api/[controller]")]
public class ProductsController : ControllerBase
{
private readonly IProductService _productService;
public ProductsController(IProductService productService)
{
_productService = productService;
}
[HttpGet("{id}")]
public async Task<IActionResult> GetProduct(int id)
{
var product = await _productService.GetProductByIdAsync(id);
if (product == null) return NotFound();
return Ok(product);
}
}
Service Layer
public interface IProductService
{
Task<Product?> GetProductByIdAsync(int id);
}
public class ProductService : IProductService
{
private readonly IProductRepository _repo;
public ProductService(IProductRepository repo)
{
_repo = repo;
}
public Task<Product?> GetProductByIdAsync(int id)
{
return _repo.GetByIdAsync(id);
}
}
Repository Layer
public interface IProductRepository
{
Task<Product?> GetByIdAsync(int id);
}
public class ProductRepository : IProductRepository
{
private readonly AppDbContext _context;
public ProductRepository(AppDbContext context)
{
_context = context;
}
public async Task<Product?> GetByIdAsync(int id)
{
return await _context.Products.FindAsync(id);
}
}
Program.cs (Dependency Injection)
builder.Services.AddScoped<IProductRepository, ProductRepository>();
builder.Services.AddScoped<IProductService, ProductService>();
Conclusion
- Keep controllers focused on handling requests, not business logic.
- Use a service layer to enforce separation of concerns.
- Repositories should be the only layer that talks directly to the database.
- With this structure, your API is easier to test, extend, and maintain.