· Jose Antonio López  · 8 min lectura

Guia @RestController a Spring Boot - CRUD Pas a Pas

Guia tècnica per a implementar un Controlador REST a Spring Boot. Millors pràctiques, principis SOLID i exemples de codi per a un CRUD professional amb maneig correcte d'estats HTTP.

Guia tècnica per a implementar un Controlador REST a Spring Boot. Millors pràctiques, principis SOLID i exemples de codi per a un CRUD professional amb maneig correcte d'estats HTTP.

Introducció

Aquesta guia descriu el meu enfocament recomanat per a desenvolupar controladors REST a Spring Boot 3.5.x. L’objectiu és crear controladors nets, mantenibles i fent servir bones pràctiques. La informació que pots trobar serveix per a la versió de Spring Boot que fa servir Servlets per a crear aplicacions web.

Les pautes que pots trobar en aquest article t’ajudaran a estructurar els teus controladors de manera efectiva, assegurant una clara separació de responsabilitats i facilitant el manteniment a llarg termini del codi. Tota la informació que pots trobar aquí està basada en la meva experiència professional desenvolupant APIs REST amb Spring Boot.

Per què escric aquest article?

Durant la meva carrera professional, he vist que molts desenvolupadors tenen una concepció errònia sobre com desenvolupar un controlador REST. A priori sembla una tasca senzilla encara que es cometen diversos errors:

  • Mala gestió des de l’inici del versionat
  • Lògica de negoci dins dels mètodes del controlador
  • APIs que no verifiquen ni comproven les dades d’entrada

Prerequisits

Per a seguir aquest article necessites tenir instal·lat:

Eina/PasEnllaç de descàrrega
JavaDescarregar Java
MavenDescarregar Maven
IDE (IntelliJ, Eclipse, VS Code, etc.)Descarregar IntelliJ
Projecte amb Spring Boot 3.5.xCrear projecte Spring Boot

Dependències

Les dependències amb les que es treballarà són:

DependènciaDescripció
spring-boot-starter-webDependències necessàries per a construir aplicacions web amb servlet. Més a la documentació oficial.
spring-boot-starter-validationSuport per a la validació de dades utilitzant l’especificació Bean Validation. Més a la documentació oficial.
lombokBiblioteca que redueix el codi repetitiu mitjançant anotacions. Més a la documentació oficial.

Decisions de disseny

Injecció de dependències mitjançant constructor

El tipus d’injecció de dependències que he fet servir és mitjançant constructor. És la millor forma d’injectar dependències a Spring i la forma recomanada a la documentació oficial.

Si acostumes a fer servir injecció de dependències mitjançant camps, et recomano que ho canviïs a injecció de dependències mitjançant constructor.

Lombok

Per reduir el codi repetitiu, he fet servir la biblioteca Lombok que redueix el codi visible mitjançant anotacions. En aquest cas, es fan servir anotacions @RequiredArgsConstructor i @Log.

Altres anotacions que es poden fer servir són @Data, @EqualsAndHashCode i @ToString encara que considero que toquen temes molt delicats i recomano implementar-los a mà si arriba el cas.

Identificador únic per a les entitats

Les entitats s’identifiquen mitjançant un UUID vàlid. Per a l’exemple faré servir UUID de versió 4 encara que en producció i nous sistemes aconsello fer servir UUID v7 ja que dóna informació sobre el temps de creació a la base de dades i ajuda a optimitzar l’indexat a la base de dades.

Sense paginació

Per simplificar evito la paginació per a retornar un llistat. En un sistema en producció real no té perquè sempre haver-hi un llistat paginat.

Per a start-ups o creació de nous prototips, aconsello sempre no incloure un llistat ja que els requeriments fluctuen moltíssim i poden succeir dues coses:

  • Canvi de codi constant
  • La funcionalitat acaba no fent-se servir

Versionats de l’API

Versionar una API és molt important perquè permet evolucionar les funcionalitats exposades al món exterior sense trencar les existents.

A Spring Boot existeixen diverses estratègies per a versionar l’API. En el meu cas per practicitat faré servir una classe estàtica pura que estarà oberta a l’extensió i no a la modificació.

Pots trobar un altre enfocament sobre el versionat d’APIs a la documentació oficial de Spring Boot

Flux d’informació a Spring fent servir Servlet

Quan una petició HTTP arriba a una aplicació Spring Boot que utilitza Servlets, segueix un flux definit abans d’arribar al controlador REST:

Imagina que una petició viatja a través de la teva aplicació Spring Boot. És com un paquet que passa per diverses estacions fins a arribar al seu destí:

  1. Tomcat: La petició HTTP arriba primer al servidor web, que per defecte és Tomcat.

  2. Filtres: Abans de continuar, la petició travessa una cadena de filtres (Filter). Un ús molt comú és per a l’autenticació o autorització, on un filtre comprova si la petició inclou un token vàlid a les capçaleres.

  3. DispatcherServlet: La petició es lliura al DispatcherServlet i consulta al HandlerMapping.

  4. HandlerMapping: Aquest component actua enllaçant la URL de la petició amb un mètode específic del controlador.

    • Exemple: Si arriba una petició GET /v1/crm-mgmt-api/customers/{id}, el HandlerMapping sap que ha de dirigir-la al mètode getCustomer(id) al CustomerController.
  5. HandlerAdapter: Un cop localitzat el mètode, el HandlerAdapter es prepara per a cridar-lo. La seva feina és mapejar les dades de la petició als paràmetres que el mètode Java espera rebre. En aquest punt és on s’apliquen les validacions dels DTOs (pots veure com crear validadors personalitzats aquí). Si alguna cosa falla, el sistema llançarà una excepció que hauria de ser capturada per un manejador global d’excepcions.

    • Exemple: Extreu l’id de la URL i el converteix en un objecte UUID per a passar-lo al mètode getCustomer(UUID id). Si fos una petició POST, converteix el JSON a un objecte DTO anotat amb @RequestBody.
  6. @RestController: Finalment, la petició arriba al teu mètode al controlador. Aquí és on s’executa el codi que has escrit.

  7. Serialització a JSON: El mètode retorna un objecte Java (per exemple, un CustomerDTO). Spring Boot, fa servir una llibreria anomenada Jackson per defecte i el converteix automàticament a format JSON. Aquest JSON s’envia de tornada al cos de la resposta HTTP juntament amb un codi HTTP d’estat.

Si vols saber més sobre l’especificació del Servlet pots consultar l’especificació oficial Servlet 6.0 que és la que s’aplica a Spring Boot 3.5.

Versió de l’API

El codi per a poder controlar el path de l’API és el següent:

package com.test.crm.config;

public class ApiConfig {
    public static final String API_VERSION_V1 = "/v1";
    public static final String COMMON_PATH = "/crm-mgmt-api";
    public static final String API_BASE_PATH_V1 = COMMON_PATH + API_VERSION_V1;

    private ApiConfig() {
        throw new UnsupportedOperationException("This class should never be instantiated");
    }
}

Explicació del codi

El codi defineix variables purament estàtiques i granulars perquè no hi hagi equivocació en quina versió està fent servir l’endpoint.

En el contracte d’una API és estrany trobar versions minor del tipus 1.1 o 1.2. Si existissin no hi hauria problema en afegir una nova variable però tingues en compte que hauria d’afegir nova funcionalitat o més dades.

Obert a l’extensió i tancat a la modificació.

DTOs

El codi és el següent:

package com.test.crm.dto;

public record CreateCustomerDTO(
    @NotBlank
    @Size(min = 3)
    @Pattern(regexp = "^[^0-9]*$", message = "Only letters")
    String name,

    @NotBlank
    @Size(min = 3)
    @Pattern(regexp = "^[^0-9]*$", message = "Only letters")
    String surname
) {
}
public record CustomerDTO(
  package com.test.crm.dto;
    UUID id,
    String name,
    String surname,
    String image,
    String createdBy,
    String lastUpdatedBy
) {
}

Controlador

El codi és el següent:

package com.test.crm.controller;

@RestController
@RequestMapping(API_BASE_PATH + "/customers")
@RequiredArgsConstructor
public class CustomerController {

    private final CustomerService customerService;

    @GetMapping("/{id}")
    public CustomerDTO getCustomer(final @PathVariable UUID id) {
        return customerService.getCustomer(id);
    }

    @GetMapping
    public List<CustomerDTO> getCustomers() {
        return customerService.getCustomers();
    }

    @PostMapping
    @ResponseStatus(HttpStatus.CREATED)
    public CustomerDTO createCustomer(final @RequestBody @Valid CreateCustomerDTO createCustomerDTO) {
        return customerService.createCustomer(createCustomerDTO);
    }

    @PatchMapping("/{id}")
    @ResponseStatus(HttpStatus.NO_CONTENT)
    public void updateCustomer(final @PathVariable UUID id, final @RequestBody @Valid UpdateCustomerDTO updateCustomerDTO) {
        customerService.updateCustomer(id, updateCustomerDTO);
    }

    @DeleteMapping("/{id}")
    @ResponseStatus(HttpStatus.NO_CONTENT)
    public void deleteCustomer(final @PathVariable UUID id) {
        customerService.deleteCustomer(id);
    }
}

Explicació del codi

¿@PatchMapping o @PutMapping?

Per a una substitució parcial o actualització de les dades cal fer servir el mètode PATCH. Si és una substitució total, llavors cal fer servir el mètode PUT.

Aquesta subtil diferència normalment es passa per alt en molts sistemes actuals a l’hora de dissenyar APIs així que és bo tenir-ho en compte.

DeleteMapping

En l’exemple l’esborrat de l’entitat es fa de forma física, és a dir les dades s’esborren de la base de dades. Existeix un altre tipus d’esborrat anomenat esborrat lògic. Consisteix en actualitzar una bandera o flag d’una dada booleana del tipus “isActive”.

Per a l’esborrat lògic prefereixo fer servir el mètode PATCH.

ResponseEntity

Molts desenvolupadors fan servir l’entitat ResponseEntity per a embolcallar els objectes de resposta en un @RestController. No obstant això, això sovint és innecessari gràcies a l’autoconfiguració de Spring Boot.

Quan un mètode d’un controlador retorna un objecte, Spring Boot fa servir Jackson (per defecte) per a serialitzar-lo automàticament a format JSON. A més, estableix la capçalera Content-Type a application/json i retorna un codi d’estat 200 OK.

Com es pot veure als mètodes getCustomer, getCustomers i createCustomer , es retorna directament el DTO (CustomerDTO o List<CustomerDTO>). Spring s’encarrega de la resta.

Quan hauries de fer servir ResponseEntity?

ResponseEntity és molt útil quan necessites un control més fi sobre la resposta HTTP. Per exemple:

  1. Establir codis dinàmics: Si un recurs no es troba, es pot retornar un ResponseEntity.notFound().build(), que es tradueix en un 404 Not Found. Pots fer una comprovació amb un early return després de la resposta. Evita fer servir un try/catch.

  2. Afegir capçaleres personalitzades: Després de crear un recurs, pot ser una bona pràctica retornar un 201 Created juntament amb la capçalera Location que conté la URL del nou recurs.

@PostMapping
public ResponseEntity<CustomerDTO> createCustomer(final @RequestBody @Valid CreateCustomerDTO createCustomerDTO) {
    final CustomerDTO createdCustomer = customerService.createCustomer(createCustomerDTO);
    final URI location = ServletUriComponentsBuilder.fromCurrentRequest()
            .path("/{id}")
            .buildAndExpand(createdCustomer.getId())
            .toUri();
    return ResponseEntity.created(location).body(createdCustomer);
}
Per què no fer servir ResponseEntity<?>?

Evita l’ús de tipus genèrics com ResponseEntity<?>. Aquesta pràctica amaga el tipus de dada real que retorna la teva API i dificulta el seu consum i la generació de documentació automàtica (com amb OpenAPI/Swagger). Sempre especifica el tipus de dada amb ResponseEntity<CustomerDTO>.

@GetMapping("/{id}")
public ResponseEntity<CustomerDTO> getCustomer(final @PathVariable UUID id) {
    return ResponseEntity.ok(customerService.getCustomer(id));
}
  • Java
  • Spring Boot
Compartir: