Clean Architecture en una aplicación web en .NET basada en CRUDs (VI): La capa de API

Introducción

En este artículo vamos a ver la última y más externa capa que va a tener nuestra aplicación web, que es la capa de API. En esta capa solamente vamos a encontrar los controladores y configuración específica de la aplicación web.

El primer controlador

Empezaremos creando un controlador. Vamos a implementar el controlador de los Player por seguir con el ejemplo que venimos usando hasta ahora durante esta serie.

Para los métodos que hemos definido, tendría la siguiente pinta (para los métodos de edición y borrado sería usar esta misma lógica):

using BasketballTournaments.Application.Players.Commands;
using BasketballTournaments.Application.Players.DTO;
using BasketballTournaments.Application.Players.Queries;
using BasketballTournaments.Application.Shared.Errors;
using FluentResults;
using MediatR;
using Microsoft.AspNetCore.Mvc;
using Swashbuckle.AspNetCore.Annotations;

namespace BasketballTournaments.Api.Players;

[Route("/api/players")]
[ApiController]
public class PlayersController : ControllerBase
{
    private readonly IMediator _mediator;

    public PlayersController(IMediator mediator)
    {
        _mediator = mediator;
    }

    [HttpGet]
    [SwaggerResponse(200)]
    [SwaggerResponse(400, "Bad request")]
    public async Task<ActionResult<IEnumerable<PlayerDto>>> Get([FromQuery] GetFilteredPlayerQuery query, CancellationToken cancellationToken)
    {
        IEnumerable<PlayerDto> result = await _mediator.Send(query, cancellationToken);
        return Ok(result);
    }

    [HttpGet("{id}")]
    [SwaggerResponse(200)]
    [SwaggerResponse(404, "Not found")]
    public async Task<ActionResult<PlayerDto>> GetById([FromRoute] Guid id, CancellationToken cancellationToken)
    {
        GetPlayerByIdQuery query = new(id);
        Result<PlayerDto> result = await _mediator.Send(query, cancellationToken);
        if (result.HasError<ItemNotFoundError>())
        {
            return NotFound();
        }

        return Ok(result.Value);
    }

    [HttpPost]
    [SwaggerResponse(201)]
    [SwaggerResponse(400, "Bad request")]
    public async Task<ActionResult<PlayerDto>> Create([FromBody] CreatePlayerCommand command, CancellationToken cancellationToken)
    {
        Result<PlayerDto> result = await _mediator.Send(command, cancellationToken);
        if (result.HasError<InvalidArgumentError>())
        {
            return BadRequest();
        }

        return CreatedAtAction(nameof(Get), result.Value);
    }
}

La ventaja, como podemos ver, de utilizar esta aproximación con clean architecture, es que el controlador solo tiene una única función: exponer los endpoints al mundo. Con esto ya tendríamos la base de nuestra aplicación lista para funcionar, y es fácilmente escalable y testeable. En los siguientes capítulos veremos cómo escribir diferentes tipos de tests, como unitarios o de integración.