Building an API that works for a simple CRUD demo is easy. Building an API that remains maintainable, testable, and performant as your team adds dozens of endpoints and entities is a different challenge. In this guide, we walk through the architectural layers of modern ASP.NET Core Web APIs.

💡
Core Rule: Dependencies should always flow inwards. High-level business policies and domain logic must never depend on low-level database libraries or HTTP framework primitives.

1. The Layer Breakdown

A resilient .NET solution typically divides responsibility into 4 distinct projects or layers:

  • Domain Layer: Pure business entities, enums, exceptions, and domain events. Zero external dependencies.
  • Application Layer: Use cases, service contracts, DTOs, CQRS handlers (MediatR), and validation rules (FluentValidation).
  • Infrastructure Layer: Database access (EF Core DbContext), external API integrations, email senders, and caching (Redis).
  • API / Presentation Layer: Controllers or Minimal APIs, middleware, authentication handlers, and dependency injection configuration.

2. Writing Slim Controllers

Controllers should do only three things: validate incoming HTTP inputs, delegate execution to an application service or mediator, and return an appropriate HTTP status code.

Controllers/ProductsController.cs
[ApiController]
[Route("api/v1/[controller]")]
public class ProductsController : ControllerBase
{
    private readonly IProductService _productService;

    public ProductsController(IProductService productService)
    {
        _productService = productService;
    }

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

        return Ok(product);
    }
}

3. Data Transfer Objects (DTOs) vs Domain Entities

Never return raw Entity Framework models directly to clients. Returning EF entities leaks database schema details and causes cyclic serialization bugs.

DTOs/ProductDtos.cs
public record CreateProductRequest(
    string Name, 
    decimal Price, 
    string Sku
);

public record ProductResponse(
    Guid Id, 
    string Name, 
    decimal Price, 
    DateTime CreatedAtUtc
);
🚀
Modern C# Tip: Use positional record types for DTOs. They provide value-based equality, immutability, and concise syntax with zero boilerplate.

4. Global Exception Middleware

Instead of wrapping every controller action in try/catch blocks, configure global error handling using the native IExceptionHandler introduced in .NET 8.

Middleware/GlobalExceptionHandler.cs
public class GlobalExceptionHandler : IExceptionHandler
{
    private readonly ILogger<GlobalExceptionHandler> _logger;

    public GlobalExceptionHandler(ILogger<GlobalExceptionHandler> logger) => _logger = logger;

    public async ValueTask<bool> TryHandleAsync(
        HttpContext context, 
        Exception exception, 
        CancellationToken ct)
    {
        _logger.LogError(exception, "Unhandled exception: {Message}", exception.Message);

        var problem = new ProblemDetails
        {
            Status = StatusCodes.Status500InternalServerError,
            Title = "Server Error",
            Detail = "An unexpected error occurred. Please try again later."
        };

        context.Response.StatusCode = problem.Status.Value;
        await context.Response.WriteAsJsonAsync(problem, ct);

        return true;
    }
}

5. Key Takeaways

  1. Keep controllers lightweight — delegate to business services or MediatR commands.
  2. Always map database entities to request/response DTO records.
  3. Rely on ASP.NET Core built-in Dependency Injection and register services with proper lifetimes (Scoped, Singleton, Transient).
  4. Use IExceptionHandler for standardized RFC 7807 Problem Details error responses.
← Back to All Browse All Tutorials Next Guide → C# Concepts Every Developer Should Know