Bonjour ! Dans la leçon précédente, vous avez brillamment mis en place une API REST complète pour les opérations CRUD sur notre entité Book. Cependant, comme nous l'avons évoqué, nos endpoints exposent directement l'entité JPA Book. Cette approche, bien que simple, présente des risques et des limitations significatifs dans un projet professionnel.
Aujourd'hui, nous allons corriger cela en introduisant l'un des patterns de conception les plus importants en développement d'API : le Data Transfer Object (DTO). L'objectif est d'apprendre à utiliser les DTOs pour créer une barrière protectrice et flexible entre le modèle de données interne de votre application (vos entités JPA) et ce que votre API expose au monde extérieur. C'est une compétence cruciale pour construire des API robustes, sécurisées et maintenables.
1. Pourquoi ne pas exposer directement les entités JPA ?
Avant de plonger dans la solution, il est essentiel de bien comprendre le problème. Exposer directement vos entités JPA via votre API est une mauvaise pratique pour plusieurs raisons.
Pourquoi et Comment Utiliser les DTOs (Data Transfer Objects ...
Cet article de CertiQuizz résume parfaitement les problèmes liés à l'exposition des entités et les avantages des DTOs.
Lisez attentivement les deux premières sections : "Introduction au pattern DTO" et "Pourquoi utiliser les DTOs ? Les avantages clés". En lisant, concentrez-vous sur les quatre problèmes majeurs listés : Le couplage fort L'exposition de données internes (sécurité) Le manque de flexibilité (over-fetching/under-fetching) Les risques de sécurité
Pour résumer, l'entité JPA est conçue pour la persistance des données. Elle représente la structure de votre table en base de données. L'API, quant à elle, définit un contrat avec les clients (par exemple, une application web front-end ou une application mobile). Ces deux préoccupations sont différentes et doivent être séparées.
Le DTO est l'objet qui matérialise ce contrat d'API. C'est un simple objet Java (souvent appelé POJO - Plain Old Java Object) dont le seul but est de transférer des données.
Le diagramme suivant illustre parfaitement cette séparation des rôles :

2. Implémentation : Utiliser un DTO pour les réponses
Passons à la pratique. Nous allons commencer par refactoriser nos endpoints GET pour qu'ils retournent un DTO au lieu de l'entité Book.
La vidéo ci-dessous montre ce processus de A à Z sur une entité User. Nous allons suivre la même logique pour notre entité Book.
Le Pattern DTO : Cas pratique sur une Application Spring Boot
Cette vidéo de "Coding With Armand" est un excellent cas pratique sur l'implémentation du pattern DTO.
Regardez d'abord l'introduction et la démonstration du problème. Vous y verrez un exemple concret de mot de passe exposé, ce qui renforce les arguments de l'article que vous venez de lire.
Maintenant, appliquons cette démarche à notre projet.
Étape 1 : Créer le DTO de réponse
Nous allons créer un package dto dans src/main/java/com/votreprojet et y ajouter une classe pour notre DTO. Une bonne pratique moderne en Java est d'utiliser un record pour les DTOs, car ils sont conçus pour être des conteneurs de données immuables.
Un record génère automatiquement le constructeur, les accesseurs (getters), ainsi que les méthodes equals(), hashCode() et toString(). C'est donc parfait pour un DTO.
Créez un fichier BookDto.java dans le package dto :
package com.votreprojet.dto;
// Ce record représente la vue "externe" d'un livre,
// ce que le client de l'API a le droit de voir.
public record BookDto(
Long id,
String title,
String author,
String isbn
) {}
Notez que nous pourrions choisir d'exposer moins de champs. Par exemple, si l'ISBN devait rester interne, il suffirait de le retirer de la définition du record.
Étape 2 : Créer le Mapper
Le "mapper" est un composant (généralement un service Spring) dont le rôle est de convertir une entité en DTO, et vice-versa.
Le Pattern DTO : Cas pratique sur une Application Spring Boot
Reprenons la vidéo pour voir comment créer un DTO et le service de mapping.
Regardez la création du DTO avec un record, puis la création du service de mapping. Observez comment le code de conversion est écrit manuellement pour chaque champ.
Inspirons-nous de la vidéo et créons notre propre BookMapper.java dans le package service :
package com.votreprojet.service;
import com.votreprojet.dto.BookDto;
import com.votreprojet.model.Book;
import org.springframework.stereotype.Service;
@Service
public class BookMapper {
public BookDto toDto(Book book) {
if (book == null) {
return null;
}
return new BookDto(
book.getId(),
book.getTitle(),
book.getAuthor(),
book.getIsbn()
);
}
}
Astuce : Pour des projets plus grands, des librairies comme MapStruct peuvent automatiser cette conversion, mais il est crucial de savoir le faire manuellement pour bien comprendre le mécanisme.
Étape 3 : Mettre à jour le Service et le Contrôleur
Maintenant, il ne nous reste plus qu'à utiliser notre BookMapper pour que nos endpoints retournent des BookDto.
Le Pattern DTO : Cas pratique sur une Application Spring Boot
La dernière étape consiste à modifier le contrôleur pour utiliser le mapper et à vérifier le résultat.
Regardez comment le contrôleur est mis à jour pour utiliser le service de mapping avec un Stream. Enfin, voyez la démonstration finale dans Postman, qui montre une réponse "propre" et sécurisée.
Modifions notre BookService pour qu'il retourne des BookDto :
// Dans BookService.java
// ... imports
import com.votreprojet.dto.BookDto;
import java.util.stream.Collectors;
@Service
public class BookService {
private final BookRepository bookRepository;
private final BookMapper bookMapper; // 1. Injecter le mapper
// Mettre à jour le constructeur
public BookService(BookRepository bookRepository, BookMapper bookMapper) {
this.bookRepository = bookRepository;
this.bookMapper = bookMapper;
}
// 2. Modifier la signature de retour et utiliser le mapper
public List<BookDto> getAllBooks() {
return bookRepository.findAll()
.stream()
.map(bookMapper::toDto) // ou .map(book -> bookMapper.toDto(book))
.collect(Collectors.toList());
}
// 3. Faire de même pour la recherche par ID
public BookDto getBookById(Long id) {
Book book = bookRepository.findById(id)
.orElseThrow(() -> new RuntimeException("Book not found with id: " + id));
return bookMapper.toDto(book);
}
// ... les autres méthodes (create, update, delete)
}
Enfin, mettons à jour le BookController pour refléter ces changements :
// Dans BookController.java
// ... imports
import com.votreprojet.dto.BookDto;
@RestController
@RequestMapping("/books")
public class BookController {
// ... constructeur
// Modifier le type de retour
@GetMapping
public List<BookDto> getAllBooks() {
return bookService.getAllBooks();
}
// Modifier le type de retour
@GetMapping("/{id}")
public BookDto getBookById(@PathVariable Long id) {
return bookService.getBookById(id);
}
// ... les autres méthodes
}
Lancez votre application et testez les endpoints GET /books et GET /books/{id} dans Postman. Vous devriez maintenant voir des réponses JSON qui correspondent à la structure de BookDto, et non plus à celle de l'entité Book.
3. DTOs pour les entrées (requêtes)
Le découplage est tout aussi important pour les données entrantes (POST, PUT). Imaginez que pour créer un livre, vous ne vouliez accepter que le titre et l'auteur, l'ISBN étant optionnel et l'ID généré par le serveur.
Pour cela, on crée un DTO spécifique pour la requête.
Pourquoi et Comment Utiliser les DTOs (Data Transfer Objects ...
L'article que vous avez consulté précédemment a une section dédiée à ce sujet.
Lisez la section "DTOs pour les entrées (requêtes) et les sorties (réponses)". Elle explique pourquoi il est utile d'avoir des DTOs distincts pour les requêtes et les réponses, et montre un exemple avec des annotations de validation.
Créons un DTO pour la création d'un livre, CreateBookRequestDto.java :
package com.votreprojet.dto;
public record CreateBookRequestDto(
String title,
String author,
String isbn
) {
// L'ID n'est pas présent, car il n'est pas fourni par le client à la création.
}
Ensuite, ajoutons une méthode de conversion inverse dans notre BookMapper :
// Dans BookMapper.java
public Book toEntity(CreateBookRequestDto dto) {
if (dto == null) {
return null;
}
Book book = new Book();
book.setTitle(dto.title());
book.setAuthor(dto.author());
book.setIsbn(dto.isbn());
return book;
}
Enfin, nous pouvons mettre à jour la méthode create dans BookService et BookController :
// Dans BookService.java
public BookDto createBook(CreateBookRequestDto requestDto) {
Book book = bookMapper.toEntity(requestDto);
Book savedBook = bookRepository.save(book);
return bookMapper.toDto(savedBook);
}
// Dans BookController.java
@PostMapping
public BookDto createBook(@RequestBody CreateBookRequestDto requestDto) {
return bookService.createBook(requestDto);
}
Nous suivrions une logique similaire pour la méthode update.
4. Exercice : Refactoriser l'API CRUD complète
Votre mission est de terminer la refactorisation de votre API :
- Créez un DTO pour les requêtes de mise à jour :
UpdateBookRequestDto.java. Il peut être similaire àCreateBookRequestDto. - Ajoutez une méthode
toEntitycorrespondante dans leBookMapper. - Mettez à jour les méthodes
updateBooketdeleteBookdans leBookServiceetBookControllerpour qu'elles utilisent les DTOs en entrée et en sortie. - Testez l'intégralité de votre API (CRUD) avec Postman pour vous assurer que tout fonctionne comme attendu. L'API ne doit plus jamais exposer ou accepter directement l'entité
Book.
Conclusion
Félicitations ! Vous maîtrisez maintenant une technique fondamentale pour le développement d'API professionnelles en Java.
Points clés à retenir :
- Découplage : Les DTOs séparent le contrat de votre API de votre modèle de données interne.
- Sécurité : Vous contrôlez précisément les champs exposés, évitant la fuite de données sensibles.
- Flexibilité : Vous pouvez faire évoluer votre base de données sans casser les clients de votre API, et inversement.
- Spécialisation : Il est courant d'avoir des DTOs différents pour les requêtes (entrées) et les réponses (sorties), adaptés à chaque cas d'usage.
- Mapping : La couche de "mapping" (conversion) est l'endroit où la traduction entre entités et DTOs s'effectue, souvent dans un service dédié.
Dans la leçon d'aujourd'hui, notre méthode getBookById lève une RuntimeException si le livre n'est pas trouvé. C'est une manière brute de gérer les erreurs. Dans un vrai projet, nous voudrions retourner une réponse HTTP 404 (Not Found) propre avec un message clair.
Dans la prochaine leçon, nous apprendrons à mettre en place une gestion des erreurs centralisée avec @ControllerAdvice et @ExceptionHandler. Cela nous permettra de gérer toutes les erreurs de notre application de manière élégante et cohérente.