CodeGym /Cursos /Spring REST & MVC /Validación en la frontera de la API

Validación en la frontera de la API

Spring REST & MVC
Nivel 15, Lección 0
Disponible

1. Problema: «que el servicio lo resuelva solo»

Cuando empiezas a escribir un backend, es muy fácil caer en la trampa de «bueno, eso es obvio, ahora lo compruebo rápidamente con ifs». Y sí, en un método pequeño parece incluso lindo: tres comprobaciones, dos throw, y eres un héroe. El problema es que la API es una puerta pública a tu casa, no un borrador personal. Y si no hay reglas claras en la entrada, empezarán a meter de todo dentro: cadenas vacías, textos enormes, null donde esperabas un valor, y «etiquetas» con un solo espacio (sí, eso también pasa).

Imagina nuestro Task Tracker API. El cliente llama a POST /api/v1/tasks y envía JSON. Si no verificas la entrada en la frontera, los datos «malos» se escapan al servicio, luego al repositorio, y luego en algún lugar interior obtienes una excepción que no se parece en nada a una respuesta contractual normal. Al cliente no le resulta más fácil: no entiende qué corregir, y tú no entiendes por qué «falla la creación de tareas, pero solo los viernes» (porque los viernes a alguien le gusta ingresar tres espacios en el title — la gente es misteriosa).

La validación en la frontera de la API resuelve precisamente este problema: convierte el «deseo suave del desarrollador» en una regla de entrada explícita, además, una que funciona igual para todos los endpoints, no solo donde no te dio pereza escribir otro if. La idea más importante aquí es simple: la capa de servicio debe ocuparse del significado de la operación («crear una tarea»), no de la desinfección de la entrada («asegurarse de que el título no esté vacío ni tenga 50000 caracteres»).

2. Frontera de la API en Spring MVC

La frase «frontera de la API» suena un poco como «frontera de un estado», y Spring Boot a veces se comporta como una aduana con mucho entusiasmo. Pero en esencia es más simple: la frontera de la API es el lugar donde el mundo exterior (cliente, navegador, aplicación móvil, servicio vecino) entra en contacto por primera vez con tu código en forma de tus tipos. En nuestro proyecto, esto es principalmente los controladores y sus argumentos: @RequestBody TaskCreateRequest, @RequestParam ..., @PathVariable ....

Si hablamos con toda honestidad, antes del controlador todavía ocurren cosas: Spring recibe la solicitud HTTP, selecciona el método handler, lee el cuerpo de la solicitud e intenta convertir JSON en DTO. Y tan pronto como el DTO ya está creado (es decir, el JSON era correcto), llega el momento ideal para la validación: tenemos un objeto, tiene campos, y podemos comprobar las reglas de «permitido/no permitido» antes de que la lógica de negocio comience a hacer algo.

Es útil tener en mente como un pequeño esquema. No porque tengas que aprender los internals de Spring, sino porque quieres entender: «en qué momento ocurre qué» — y dejar de tratar al framework como un bosque mágico.

flowchart TD
    A[Solicitud HTTP] --> B["HttpMessageConverter: JSON -> DTO"]
    B --> C["Validación de entrada: restricciones"]
    C --> D["Método del controlador"]
    D --> E["Capa de servicio"]

Nota un detalle importante. Si el JSON está roto, entonces la flecha «JSON -> DTO» no funcionará, y la validación no llegará a ocurrir. Esto es normal: no puedes validar un objeto que no pudiste crear. Por lo tanto, «JSON roto» y «DTO no pasa las reglas» son dos clases diferentes de problemas, y es útil distinguirlas desde ahora, incluso sin profundizar en el formato de errores.

3. Comprobaciones manuales y «contrato desparramado»

Las comprobaciones manuales no son «malvadas». El mal es cuando se convierten en tu forma principal de describir el contrato, y luego se extienden por el proyecto en una capa delgada. Primero verificas title en create, luego olvidas verificarlo en update, luego agregas una comprobación de longitud de la descripción, pero solo en un lugar, y luego cambias el límite de 2000 caracteres a 3000 — y adivina cuántos lugares necesitas corregir. Correcto: tantos como ya no recuerdas.

Mira una implementación «manual» típica. Parece lógica hasta que imaginas que habrá decenas de métodos así.

public void create(TaskCreateRequest request) {
    // Comprobación de campo obligatorio: sin título no se puede crear la tarea
    if (request.title() == null || request.title().isBlank()) {
        // Desventaja de este enfoque: cada uno escribe el formato del error como quiere
        throw new IllegalArgumentException("title is required");
    }

    // Comprobación de límite de longitud: de lo contrario, puede volar una «sábana» a la base de datos/log/interfaz
    if (request.description() != null && request.description().length() > 2000) {
        throw new IllegalArgumentException("description is too long");
    }
}

En este punto, puede parecer que el problema es solo «código adicional». Pero el verdadero problema es otro: la regla «qué es una solicitud válida» ahora está oculta dentro del método. El cliente no lo sabe, el controlador no lo sabe, las pruebas lo descubren por casualidad, y otro desarrollador solo se entera después del tercer intento de enviar una solicitud y recibir otro IllegalArgumentException.

Otra cosa desagradable: las comprobaciones manuales crean casi inevitablemente diferentes comportamientos para diferentes endpoints. Por ejemplo, un método lanzará IllegalArgumentException con el texto "title is required", otro con "Bad title", y un tercero directamente NullPointerException, porque alguien olvidó la comprobación de null y fue al trim(). Al final, el cliente no recibe un contrato, sino «adivina la melodía».

Y finalmente, las comprobaciones manuales con if mantienen muy mal la disciplina de capas. Puedes comenzar fácilmente a hacer «un poquito de validación» en el controlador, «un poquito» en el servicio y «bueno, un par de comprobaciones más» en el repositorio. El código funcionará, pero leerlo y mantenerlo se volverá doloroso: las reglas de entrada se extienden por el proyecto como un gato sobre el teclado — parece lindo, pero el resultado es dudoso.

4. DTO y restricciones: contrato de entrada explícito

Cuando decimos «validación en la frontera», no significa «escribamos otra capa con un montón de magia». Al contrario, la idea es muy terrenal: si una regla se refiere a un campo del DTO de entrada, entonces debe expresarse junto a ese campo. Entonces el DTO se convierte de un «contenedor de transporte» en una pequeña especificación: qué es obligatorio, cuáles son los límites de longitud, qué formatos son aceptables.

En Task Tracker API tenemos restricciones de negocio para la tarea: el título es obligatorio y tiene una longitud de 3..120, la descripción puede faltar, pero si está, no debe exceder 2000. Y esto es un candidato ideal para la validación declarativa. Porque la regla no depende del estado de la aplicación, no requiere conocimiento de la base de datos, no requiere lógica compleja. Es una norma sanitaria: como «no se puede ingresar una motosierra al local». No porque estemos en contra de las motosierras, sino porque en la oficina suelen estorbar.

Para este fragmento nos bastan dos campos: title y description. Los demás campos del DTO de solicitud de proyecto los omitimos conscientemente aquí para no perder el principio: cómo la restricción vive junto al campo.

Ejemplo de DTO que lleva sus propias reglas:

import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.Size;

public record TaskCreateRequest(
        // Campo obligatorio: no se puede pasar null/cadena vacía/solo espacios
        @NotBlank
        // Contrato de longitud: de 3 a 120 caracteres
        @Size(min = 3, max = 120)
        String title,

        // El campo puede faltar, pero si está — limitamos la longitud máxima
        @Size(max = 2000)
        String description
) {}

Observa la magia de «ingeniería honesta»: nada está oculto, todo se lee. Abriste el archivo — viste el contrato. No hace falta buscar en qué servicio y en qué método decidieron hoy que title debe tener al menos 3 caracteres.

Esto también ayuda a mantener la uniformidad entre create y update. Si un campo tiene el mismo significado en create y update, entonces las restricciones básicas generalmente deben coincidir. Y si mañana decides que el título puede tener hasta 140 caracteres (porque el gestor de tareas se convirtió repentinamente en una plataforma poética), cambias la regla en un solo lugar — en el DTO. No en diez servicios ni en cinco controladores.

5. Controlador delgado: «punto de control»

Hay un miedo que casi siempre aparece en los principiantes: «Si validamos en la frontera, ¿significa que el controlador se volverá inteligente y gordo?» No. Al contrario: una validación correcta hace que el controlador sea aún más delgado. Deja de ser el lugar donde verificas manualmente cada campo y se convierte en lo que debe ser: un punto de entrada que acepta el contrato, activa la validación y lo pasa adelante.

En Spring MVC se ve como una acción muy pequeña pero muy importante: le dices al framework «este argumento DTO debe ser validado». Y ya está. El controlador no se convierte en juez, sigue siendo un torniquete.

import jakarta.validation.Valid;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;

@RestController
@RequestMapping("/api/v1/tasks")
class TaskController {

    @PutMapping("/{taskId}")
    public ResponseEntity<TaskDetailsResponse> update(
            // Tomamos el identificador del parámetro de ruta de la URL
            @PathVariable String taskId,
            // Pedimos a Spring que valide el DTO según las restricciones antes de entrar en la lógica de negocio
            @Valid @RequestBody TaskPutRequest request
    ) {
        // Si el DTO no es válido, normalmente ni siquiera se llegará a esta línea
        var updated = taskService.update(taskId, request);
        return ResponseEntity.ok(taskMapper.toDetailsResponse(updated));
    }
}

Sí, aquí aparece @Valid, y parece «otra anotación». Pero el significado es más importante que la sintaxis: has activado la verificación sistemática de entrada. Ahora el controlador no tiene que escribir if (title == null || title.isBlank()) y no tiene que decidir qué excepción lanzar. Simplemente declara: «Este objeto debe cumplir las reglas descritas en él». ¿Y dónde están las reglas? En el DTO. Por lo tanto, las reglas no están dispersas por el código, sino que se encuentran en un lugar predecible.

Y además: ese controlador es más fácil de leer. Ves la firma del método y casi de inmediato entiendes el contrato: taskId se toma del path, el cuerpo — del JSON, el cuerpo debe ser válido. Esto ya no es una «función aleatoria», sino una declaración del comportamiento de la API.

6. Capa de servicio: menos ruido, más significado

La capa de servicio en nuestro curso es el lugar donde reside la acción aplicada: crear una tarea, actualizar una tarea, cambiar el estado, añadir un comentario. Si el servicio se ve obligado a ocuparse constantemente de la comprobación manual de cadenas, rápidamente se convierte en un largo código de «fontanería», donde la lógica de negocio se ahoga en comprobaciones, trim(), isBlank() e intentos de explicar al cliente qué es exactamente lo que está mal.

Cuando trasladas las reglas básicas de validación a la frontera de la API y las describes en el DTO de solicitud, el servicio comienza a trabajar con una entrada más limpia. No se vuelve «absolutamente confiado», pero la desinfección básica sale de él. Y el código se acerca más al significado, no a la lucha contra el caos.

public Task update(String taskId, TaskPutRequest request) {
    // Aquí asumimos que la comprobación sanitaria básica ya se realizó en la frontera de la API
    Task task = repository.getById(taskId);

    // El servicio se ocupa de la actualización semántica de la entidad, no de la validación repetitiva de cadenas
    task.setTitle(request.title());
    task.setDescription(request.description());
    task.setAssigneeName(request.assigneeName());
    task.setPriority(request.priority());

    // Guardamos el estado final (y ya en este nivel son posibles comprobaciones de dominio)
    return repository.save(task);
}

Es importante leer este ejemplo correctamente. No dice: «en el servicio nunca habrá comprobaciones». Dice: «en el servicio no debe haber comprobaciones sanitarias repetitivas, que es lógico y más barato procesar en la frontera». El servicio ahora puede concentrarse en el significado de la actualización, en las reglas del dominio, en trabajar con el almacenamiento y en la coherencia de las operaciones. Y no en que alguien volvió a enviar title con espacios.

7. Escenarios negativos sistemáticos

Cuando la entrada se verifica sistemáticamente, no solo cambia el código, sino también el comportamiento de la API como producto. Aparece la previsibilidad: si el cliente envió una solicitud no válida, no «a veces falla», no «a veces crea datos extraños», no «a veces devuelve una excepción incomprensible». La solicitud se detiene en la frontera. Y esto es importante, porque la frontera es el lugar donde nosotros, como desarrolladores, todavía podemos decir honestamente: «Usted ha enviado algo incorrecto».

La validación en la frontera también hace que los escenarios negativos sean reproducibles. Si la regla está expresada en el DTO y activada en la entrada, no olvidarás aplicarla en otro endpoint «por descuido». El error deja de ser una lotería. Para la API esto es una gran diferencia: el código del cliente comienza a confiar en que si la solicitud fue aceptada y pasó las reglas básicas, entonces se puede hablar de problemas aplicativos reales, no de «basura técnica en la entrada».

Y otro punto: este enfoque ayuda a tu equipo. Un DTO con restricciones es un documento que se lee directamente en el código. Incluso si no consultas ninguna documentación, incluso si la colección de Postman se perdió, incluso si el README está escrito «como sea», las reglas de entrada siguen siendo explícitas. Y esto hace que el proyecto madure sin necesidad de infraestructura pesada.

8. Errores típicos en la validación en la frontera de la API

Error n.º 1: comprobar lo mismo en dos lugares «por si acaso».
Reacción muy humana: añadiste restricciones en el DTO, pero dejaste las viejas comprobaciones if en el servicio o en el controlador, porque «nunca se sabe». Al final obtienes duplicación que casi inevitablemente divergirá: en un lugar el límite es 120, en otro 100, y en un tercero simplemente olvidaron actualizar. Es mejor elegir una frontera principal para la comprobación sanitaria básica y mantenerla allí de manera consistente.

Error n.º 2: ocultar las reglas de entrada dentro del servicio, porque «los controladores deben ser tontos».
Los controladores realmente deben ser delgados, pero delgado no significa «irresponsable». Un controlador delgado es aquel que recibe la solicitud, activa los mecanismos infraestructurales de la frontera (incluyendo la validación) y delega el trabajo semántico al servicio. Si ocultas las reglas de entrada en el servicio, mezclas responsabilidades: el servicio comienza a protegerse de la basura en lugar de ocuparse de la acción.

Error n.º 3: considerar el DTO como «simplemente una caja para datos» y no tratarlo como un contrato.
En una API REST, el DTO de solicitud es parte del contrato público. Si no estableces reglas para title, description, assigneeName, entonces el contrato se vuelve implícito. Los clientes empiezan a adivinar qué está permitido y qué no, y a menudo «adivinan» hacia el caos. Un DTO con restricciones hace visibles las reglas y protege la API de la flexibilidad accidental.

Error n.º 4: lanzar IllegalArgumentException como respuesta universal a todo.
Cuando las comprobaciones están dispersas por el proyecto, el desarrollador a menudo elige la salida más rápida: «si está mal, lanza IllegalArgumentException». A corto plazo funciona, pero a largo plazo lleva a que diferentes métodos lancen diferentes excepciones, con diferentes textos, y la API se vuelve impredecible. La validación declarativa precisamente disciplina esta capa: la comprobación se vuelve sistemática, no «cada uno escribe como quiere».

Error n.º 5: olvidar activar la validación en cascada de objetos anidados.
Si en el DTO de solicitud hay objetos anidados o listas (por ejemplo, List<SubtaskCreateRequest>), las restricciones en los elementos anidados no se activarán por sí solas. Es necesario marcar explícitamente el campo como @Valid, de lo contrario estarás seguro de que «la validación está activada», pero en la práctica parte de la entrada pasará tranquilamente a la capa de servicio.

1
Tarea
Spring REST & MVC, nivel 15, lección 0
Bloqueada
Creación de nota sin `if` manual en el servicio
Creación de nota sin `if` manual en el servicio
1
Tarea
Spring REST & MVC, nivel 15, lección 0
Bloqueada
Actualizacion de ficha de autor con reglas en el DTO
Actualizacion de ficha de autor con reglas en el DTO
Comentarios
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION