Código de ejemplo construidoLos DTOs y el controlador de esta página desarrollan la especificación de la actividad (campos de validación, rutas, códigos HTTP) con nombres y reglas fieles al enunciado. Revísalos antes de darlos por buenos.

1DTOs con Bean Validation

Un DTO es un objeto separado de la entidad de dominio que representa los datos tal como llegan por HTTP. Así evitas anotar el modelo de negocio con restricciones específicas de un endpoint.

java
public record NuevoExpedienteRequest(
    @NotBlank(message = "El número de expediente es obligatorio")
    String numero,

    @NotNull(message = "El importe es obligatorio")
    @DecimalMin(value = "0.0", inclusive = false,
            message = "El importe debe ser mayor que 0")
    BigDecimal importe,

    @NotBlank
    @Pattern(regexp = "\\d{4}[A-Z]{3}",
            message = "Matrícula con formato inválido")
    String matricula) { }
java
public record ExpedienteResponse(
    String numero,
    EstadoExpediente estado,
    BigDecimal importe) {

    public static ExpedienteResponse desde(Expediente expediente) {
        return new ExpedienteResponse(
                expediente.numero(), expediente.estado(), expediente.importe());
    }
}
AnotaciónTipoComprueba
@NotNullCualquieraNo es null
@NotBlankStringNo vacío ni solo espacios
@Size(min=, max=)String/colecciónLongitud en rango
@DecimalMin / @DecimalMaxBigDecimalRango con precisión decimal
@PositiveNuméricosEstrictamente positivo
@Pattern(regexp=)StringCoincide con una expresión regular

2El controlador

El controlador depende de la interfaz de service, nunca de repository directamente.

java
@RestController
@RequestMapping("/api/expedientes")
public class ExpedientesController {
    private final ServicioExpedientes servicio;

    public ExpedientesController(ServicioExpedientes servicio) {
        this.servicio = servicio;
    }

    @PostMapping
    @ResponseStatus(HttpStatus.CREATED)
    public ExpedienteResponse crear(@Valid @RequestBody NuevoExpedienteRequest peticion) {
        var expediente = servicio.registrar(
                peticion.numero(), peticion.importe(), peticion.matricula());
        return ExpedienteResponse.desde(expediente);
    }

    @GetMapping("/{numero}")
    public ExpedienteResponse obtener(@PathVariable String numero) {
        return ExpedienteResponse.desde(servicio.obtener(numero));
    }
}
Sin @Valid no hay validaciónSi el método del controlador recibe @RequestBody sin @Valid, las restricciones del DTO se ignoran completamente y cualquier dato pasa.

3Manejo centralizado de errores

Sin manejador propio, una petición inválida devuelve un 400 genérico sin decir qué campo falló.

java
@RestControllerAdvice
public class ManejadorErrores {

    @ExceptionHandler(MethodArgumentNotValidException.class)
    public ResponseEntity<Map<String, String>> manejarValidacion(
            MethodArgumentNotValidException ex) {
        Map<String, String> errores = new LinkedHashMap<>();
        ex.getBindingResult().getFieldErrors().forEach(error ->
                errores.put(error.getField(), error.getDefaultMessage()));
        return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(errores);
    }

    @ExceptionHandler(ExpedienteNoEncontradoException.class)
    public ResponseEntity<String> manejarNoEncontrado(ExpedienteNoEncontradoException ex) {
        return ResponseEntity.status(HttpStatus.NOT_FOUND).body(ex.getMessage());
    }
}

4Verificación manual

Con la aplicación arrancada (./mvnw spring-boot:run), prueba los tres casos con curl o un cliente REST.

bash
# Petición válida → 201 Created
curl -i -X POST http://localhost:8080/api/expedientes \
  -H "Content-Type: application/json" \
  -d '{"numero":"EXP-0001","importe":150.00,"matricula":"1234ABC"}'

# Número en blanco + importe negativo → 400, ambos campos en el error
curl -i -X POST http://localhost:8080/api/expedientes \
  -H "Content-Type: application/json" \
  -d '{"numero":"","importe":-10,"matricula":"1234ABC"}'

# Expediente inexistente → 404
curl -i http://localhost:8080/api/expedientes/NO-EXISTE
Regla práctica: dónde va cada comprobaciónBean Validation comprueba la forma, no el significado. Que un DNI ya exista requiere consultar el repositorio: eso es lógica de negocio y pertenece al service, no al DTO.

!Errores frecuentes

Sin error en un campo vacíoFalta @Valid en el parámetro del controlador.
400 sin detallesNo hay un @RestControllerAdvice configurado.
Campo anidado inválido sin errorFalta @Valid en ese campo, o en cada elemento de una lista (List<@Valid Tipo>).

✓Criterios de aceptación

  • El controlador depende de la interfaz de service, nunca de repository
  • El DTO de entrada usa Bean Validation y el método lleva @Valid
  • Una petición inválida responde 400 con el detalle por campo
  • Un expediente inexistente responde 404
  • El DTO no valida duplicados: eso es lógica de negocio del service

Adaptado de apartado g — Controladores REST y validación de entrada.