El espejismo de validar en el controlador
20-07-2026
En el desarrollo de software es muy común el debate sobre dónde colocar las validaciones. A menudo, nos vemos tentados a aprovechar la magia de los frameworks y acoplar todas las reglas en el controlador web bajo la excusa del pragmatismo y el ahorro de tiempo.
Pero seamos sinceros: con la asistencia de la Inteligencia Artificial actual, ese coste de tiempo que te ahorrabas al escribir todo rápido en el controlador ya no tiene sentido. Hoy en día, generar un buen modelo de dominio rico y bien estructurado cuesta exactamente los mismos segundos de tecleo que acoplarse a la infraestructura. La excusa del “presupuesto de tiempo” ha caducado.
La trampa de los casos sencillos y la complejidad accidental
Por supuesto, existen escenarios excepcionales. Si bien puede parecer una buena idea saltarse el diseño guiado por el dominio cuando solo tenemos un webhook que ingesta datos o un CRUD básico, la realidad es que esto genera una profunda asimetría en el proyecto.
En el ecosistema de C#, las Minimal APIs proporcionan un valor añadido brutal para generar controladores muy sencillos. Pero si un controlador valida la petición HTTP y además interactúa directamente con la base de datos, sabrá demasiado sobre cómo se comporta el negocio, contaminándose con información innecesaria. El controlador web debe limitarse a conocer el protocolo de comunicaciones, mientras que la capa de negocio debe ser ignorante de ese entorno.
Si validas en el controlador y el negocio evoluciona, tendrás que reescribir la infraestructura, lo cual viola el principio Abierto/Cerrado (OCP), ya que tu código no estará cerrado a la modificación ante cambios en las reglas de negocio. No se trata de programar hoy soluciones extremadamente genéricas para anticipar un futuro incierto, sino diseñar el código para que la complejidad accidental no crezca exponencialmente.
Parsear, no validar: el dominio resguarda sus reglas
Si el controlador no debe validar, ¿quién lo hace? La respuesta reside en el concepto de “Parsear, no validar”. El controlador recibe la petición y la pasa al Caso de Uso, y este actúa como un parser: consume una entrada desconfiable y la transforma en tipos fuertemente estructurados.
Para que esto funcione, el dominio debe asegurar que los estados ilegales sean irrepresentables. Al instanciar un Value Object (como un nombre o un email), su constructor actúa como un guardián implacable. Si los datos no cumplen con las reglas de negocio, el objeto simplemente se niega a existir lanzando una excepción. El controlador, que ahora es un simple adaptador de red, captura esa excepción y la traduce al cliente.
Veamos cómo se refleja esto en el código, donde el controlador delega todo el peso al Caso de Uso y las reglas quedan protegidas en el dominio, utilizando mensajes de error en inglés:
// 1. DTO Inmutable (Garantiza que no hay nulos estructurales) public record UserRegistrationRequest(string FirstName, string Email); // 2. Controlador como Humble Object Pattern app.MapPost("/users", (UserRegistrationRequest request, RegisterUserUseCase useCase) => { try { // Delegamos todo el peso al Caso de Uso pasando la petición cruda useCase.Execute(new RegisterInfo(Name: request.FirstName, Email: request.Email)); return Results.Ok(new { Message = "User registered successfully." }); } catch (ArgumentException ex) { // El controlador solo traduce el rechazo del dominio a protocolo HTTP return Results.BadRequest(new { Error = ex.Message }); } });
// 3. El Caso de Uso "parsea" la información public class RegisterUserUseCase { private readonly IUserRepository _repository; public RegisterUserUseCase(IUserRepository repository) { _repository = repository; } public void Execute(RegisterInfo info) { // La validación de negocio ocurre intrínsecamente al instanciar el dominio. // Si el código pasa de estas líneas, el estado es 100% válido. var firstName = UserFirstName.Create(info.FirstName); var email = EmailAddress.Create(info.Email); var user = new User(firstName, email); _repository.Save(user); } }
// 4. Value Object resguarda la invariante de negocio public class UserFirstName { private readonly string _email; private UserFirstName(string givenName) => _email = givenEmail; public static UserFirstName Create(string givenName) { // Aplicamos reglas de negocio reales directamente en el constructor if (string.IsNullOrWhiteSpace(givenName)) throw new ArgumentException("First name cannot be empty"); if (givenName.Length > 50) throw new ArgumentException("First name exceeds the 50 characters limit"); if (givenName.Any(char.IsDigit)) throw new ArgumentException("First name cannot contain numbers"); return new UserFirstName(givenName); } public override string ToString() => _email; }
El éxodo de los tests y la testeabilidad del sistema
El argumento definitivo para sacar las validaciones del framework web es la testeabilidad.
Cuando acoplas tus validaciones a las anotaciones del controlador, te ves obligado a levantar el contexto web mediante costosos tests de integración para verificar si una petición es rechazada o aceptada. Tratar de cubrir todos los casos límite y reglas de negocio a través de pruebas en las fronteras de la aplicación conduce irremediablemente a una explosión combinatoria de pruebas.
Al aislar las validaciones dentro del Caso de Uso y apoyarte en Value Objects puros como UserFirstName, logras beneficios masivos para tu estrategia de pruebas:
- Aislamiento de I/O: Empujas las operaciones impuras (como leer la petición HTTP) hacia los bordes del sistema, dejando el núcleo del negocio aislado. El Caso de Uso orquesta el flujo de la información, pero la lógica real está en el dominio.
- Tests unitarios ultrarrápidos: Evaluar si un nombre rechaza textos vacíos o números se hace ahora invocando el constructor de
UserFirstNamecientos de veces en milisegundos, evaluando cada combinación sin necesidad de iniciar una base de datos ni servidor web. El propio sistema de tipos actúa como barrera de integridad. - Tests sostenibles: Un código sostenible exige que sus tests también lo sean. Modificar reglas de negocio testeables unitariamente previene la fricción y el miedo al cambio que ocurre cuando dependes exclusivamente de tests de integración end-to-end.
Conclusión
Al final del día, sacar las validaciones de la capa web y llevarlas al dominio no es un dogma inquebrantable. Ningún diseño es exclusivamente bueno; en el desarrollo de software, todo se resume a una constante evaluación de trade-offs o compromisos. Habrá escenarios muy concretos —como una pequeña herramienta interna o un prototipo desechable— donde quizá decidas que la inmediatez justifica utilizar las validaciones mágicas del framework web.
Sin embargo, ahora somos un poco más conscientes de este trade-off: sabemos exactamente qué estamos entregando a cambio. Entendemos que al cederle esa responsabilidad al controlador, estamos apostando en contra de la testeabilidad, empujando el diseño hacia un Modelo de Dominio Anémico y asumiendo un riesgo de complejidad accidental que puede crecer de forma exponencial hasta hacernos perder el control.
Elige tus trade-offs con la mente clara, pero recuerda: es tu dominio el que debe mandar.
Referencias bibliográficas
-
Código Sostenible (Carlos Blé Jurado):
- Peligros de la inercia del código: La complejidad accidental puede crecer exponencialmente si no se aplican buenos diseños a tiempo, haciendo que el equipo pierda el control del proyecto (Página 23).
- Separación de responsabilidades: El controlador web solo debe conocer el protocolo HTTP para gestionar peticiones y respuestas, mientras que la capa de negocio no conoce nada del entorno (Páginas 73-74).
- Principio Abierto-Cerrado (OCP): Un artefacto debe admitir ampliaciones de comportamiento sin necesidad de modificar su código base (Página 89).
- Tests Sostenibles: El código de prueba es igual de importante que el de producción y debe ser fácil de mantener en el tiempo (Página 39).
-
Domain Modeling Made Functional (Scott Wlaschin):
- Hacer los estados ilegales irrepresentables: Forzar a que los constructores capturen las reglas de negocio para asegurar la integridad de los datos (Página 292).
- Aislamiento de la infraestructura: Empujar las dependencias de Entrada/Salida (bases de datos, red) a los bordes de la aplicación y mantener el núcleo de dominio aislado creando fronteras de confianza (Página 283).
-
Code That Fits in Your Head (Mark Seemann):
- Parse, don’t validate: Un parser consume entradas poco estructuradas y produce salidas fuertemente tipadas (Página 162 / Sección 7.2.5).
- El Objeto Humilde (Humble Object): Retirar la lógica de las capas de infraestructura difíciles de testear para que no requieran pruebas exhaustivas (Página 113).
- Explosión combinatoria: Intentar cubrir todas las reglas de negocio a través de la frontera de la API conduce a una cantidad inmanejable de tests de integración (Página 107).
-
Domain-Driven Design Distilled (Vaughn Vernon):
- Modelo de Dominio Anémico: Ocurre cuando se permite que la lógica de negocio se filtre hacia los controladores o servicios de aplicación en lugar de recaer sobre los objetos del dominio (Página 89).
