REST Api

TL;DR

  • Les Data Transfert_Object sont utilisés en I/O. Permet de simplifier les échanges.

DTO

C'est un objet qui permet de cacher au niveau de l'API le vrai contenu des entités. Ils sont gérés par les Controllers. Un DTO vient toujours avec un Mapper. Les record sont particulièrement adaptés à cela.

Example

  • Entity:
@Entity
public class Person {
    private String name;
    private String gender;
    private String address;

    /* Getters / Setters */
}
  • DTO (souvent un record mais attention à la version de Spring):
public record PersonDto(
    String name,
    String gender
) {}
  • Mapper (soit un composant Spring, soit une classe "static"):
@Component
public class PersonDtoMapper {
    public PersonDto fromModel(Person p) {
        return new PersonDto(
            p.getName(),
            p.getGender()
        );
    }
}

JsonView (peu utilisé)

@JsonView définit un ensemble de propriétés qui seront exportées en JSON. On peut annoter les propiétés avec une vue pour qu'elle soit associée à celle-ci. Quand un controller mapping est annoté avec une @JsonView, il utilisera les gens qui fonctionnent pour produire la vue associé en JSON. On peut utiliser l'héritage pour étendre le scoe des vues.

Example

  • Entity:
@JsonView(Views.LIGHT.class)
public class Person {
    @JsonView(View.ID.class)
    private Long id;

    private String name;
    private String gender;
    private String address;

    public static class Views {
        public interface ID {}
        public interface LIGHT extends ID {}
        public interface PAGE extends LIGHT {}
        public interface FULLL extends PAGE {}
    }
}
  • Method call:
@GetMapping
@JsonView(Person.Views.LIGHT.class)
public Page<Person> fetchPage() {
    return personService.findAll();
}

JsonIdentity

JsonIdentityInfo

Permet de dédupliquer les champs envoyés:

@JsonIdentityInfo(generator = ObjectIdGenerators.PropertyGenerator.class, property = "id")
public class Person { /* ... */}

JsonTypeInfo

Ajoute un field type pour les cas où il y a du polymorphisme. Cela permet de désérialiser dans le front.

  • Par exemple, avec une classe mère:
@JsonTypeInfo(use = JsonTypeInfo.Id.NAME, defaultImpl = Product.class)
@JsonSubTypes({
        @JsonSubTypes.Type(value = Book.class),
        @JsonSubTypes.Type(value = VideoGame.class),
        @JsonSubTypes.Type(value = Phone.class)
})
public class Product { /* ... */ }

  • Dans la classe fille:
@JsonTypeName("book")
public class Book extends Product { /* ... */ }

Resources & HTTP statuses

Status codes

  • 2XX
    • 200 OK
    • 201 CREATED
  • 3XX
  • 4XX:
    • 400: Bad Request
    • 401: Unauthorized
    • 403: Forbidden
    • 404: Not Found
    • 406: Not acceptable
    • 409: Conflict
    • 410: Gone
  • 5XX:
    • 500: Internal Server Error

In Spring

@ResponseStatus(HttpStatus.status)
<type> <method_name>(){}

Exception Handler

Cf here

Possible de faire une entity pour renvoyer des erreurs. Ça permet de controller la granularité.

  • Exception avec l'annotation @ResponseStatus sont retournés avec le bon http code.
  • Exception Handler pour controlleur unique:
public class Controller {
    @ExceptionHandler({ Exception1.class, Exception2.class})
    public void handler() {
        // Handle Here
    }
}
  • DefaultHandlerExceptionResolver définit par défaut à partir de Spring.3
  • ResponseStatusExcetionREsolver: permet de redéfinir avec un @ResponseStatus le code d'erreur des exceptions. Il faut que la classe avec le @ResponseStatus extends RuntimeException.
  • @ControllerAdvice permet de faire un handler global:
@ControllerAdvice
public class RestResponseEntityExceptionHandler 
  extends ResponseEntityExceptionHandler {

    @ExceptionHandler(value 
      = { IllegalArgumentException.class, IllegalStateException.class })
    protected ResponseEntity<Object> handleConflict(
      RuntimeException ex, WebRequest request) {
        String bodyOfResponse = "This should be application specific";
        return handleExceptionInternal(ex, bodyOfResponse, 
          new HttpHeaders(), HttpStatus.CONFLICT, request);
    }
}
  • For REST use @RestControllerAdvice instead.

Reminder:

  • DO:

    • If you need an error code/message, put it in the exception at the time you throw it
    • Use @ControllerAdvice or @RestControllerAdvice to handle all general purpose exceptions
    • Box all exceptions into a single ApiException with a uniformed shape
  • DON'T:

    • Try-catch business-level exception everywhere in your controllers, to box them into ApiException with error message.
    • Write your own custom exception where java.lang or java.util has already a standard exception for the same purpose eg: Do not write NotFoundException, use java.util.NoSuchElementException.
    • Let your API output exceptions of various shape

Hateoas

HAL - Hypertext Application Language

  • Import du package suivant:
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-hateoas</artifactId>
</dependency>
  • Pour mettre un lien dans le header location, il faut modifier le code Java avec:
    import static org.springframework.hateoas.server.mvc.WebMvcLinkBuilder.linkTo;
    import static org.springframework.hateoas.server.mvc.WebMvcLinkBuilder.methodOn;
    
    public ResponseEntity<Customer> create(/* Args */) {
        Customer c = createCustomer();
        URI uri = linkTo(methodOn(CustomerApi.class).getCustomer(customer.getId())).toUri();
        return ResponseEntity.created(uri).body(customer);
    }
  • Pour rajouter les liens dans le corps du Json, il faut utiliser:
    public ResponseEntity<Customer> create(/* Args */) {
        Customer c = createCustomer();
        Link selfLink = linkTo(methodOn(CustomerApi.class).getCustomer(customer.getId())).withSelfRel();
        Link cartLink = linkTo(methodOn(CartApi.class).getCart(customer.getId(), customer.getCart().getId())).withRel("cart");
        return ResponseEntity.created(selfLink.toUri()).body(EntityModel.of(customer, selfLink, cartLink));
    }
  • On peut utiliser le RepresentationModelAssembler pour créer les liens des entités automatiquement (comme les serializers et les generators).

OpenAPI (ex Swagger)

  • To generate the OpenAPI specification, we have to add the following packages:
<!-- Spring OpenAPI doc -->
<dependency>
    <groupId>org.springdoc</groupId>
    <artifactId>springdoc-openapi-ui</artifactId>
    <version>${springdoc.version}</version>
</dependency>
<!--support for hateoas -->
<dependency>
    <groupId>org.springdoc</groupId>
    <artifactId>springdoc-openapi-hateoas</artifactId>
    <version>${springdoc.version}</version>
</dependency>
<!-- support for Pageable-->
<dependency>
    <groupId>org.springdoc</groupId>
    <artifactId>springdoc-openapi-data-rest</artifactId>
    <version>${springdoc.version}</version>
</dependency>
  • The Bean should be added to the configuration:
OpenAPI openApi() {
    Contact contact = new Contact();
    contact.setName("Étienne Marais");
    contact.setEmail("[email protected]");
    return new OpenAPI().info(new Info()
        .contact(contact)
        .title("Takima-store Backend API")
        .license(new License().name("ISC")));
}
  • The API is available on:
  • These annotations can be used to be more user friendly:
    • @Parameter:
    • @Schema:
    • @Content:
    • @Tag: Marque une classe comme une ressource swagger
    • @Operation: Décrit une opération ou une méthode HTTP spécifique pour le chemin en question
    • @ApiResponse: Décrit une réponse possible à l'opération