API REST avec Spring Boot
Notions théoriques
@RestController vs @Controller
Un @RestController est une combinaison de @Controller et @ResponseBody. Chaque méthode retourne directement des données JSON (sérialisées automatiquement par Jackson), et non le nom d'une vue Thymeleaf.
@Controller // Retourne le nom d'une vue Thymeleaf
public class ArticleController {
@GetMapping("/articles")
public String liste(Model model) {
model.addAttribute("articles", articleRepository.findAll());
return "articles/liste"; // Nom d'un template HTML
}
}
@RestController // Retourne du JSON directement
public class ArticleApiController {
@GetMapping("/api/articles")
public List<Article> liste() {
return articleRepository.findAll(); // Jackson sérialise en JSON
}
}
Jackson (inclus avec Spring Boot) sérialise automatiquement les objets Java en JSON en utilisant les getters.
ResponseEntity : contrôler le code HTTP
ResponseEntity<T> permet de contrôler précisément le code de statut HTTP retourné :
// 200 OK avec corps
ResponseEntity.ok(article)
// 201 Created avec en-tête Location et corps
URI location = URI.create("/api/articles/" + article.getId());
ResponseEntity.created(location).body(article)
// 404 Not Found sans corps
ResponseEntity.notFound().build()
// 204 No Content (suppression réussie)
ResponseEntity.noContent().build()
// 400 Bad Request
ResponseEntity.badRequest().body("Message d'erreur")
Recevoir du JSON avec @RequestBody
@RequestBody demande à Spring de désérialiser le JSON reçu dans le corps de la requête HTTP en objet Java :
@PostMapping("/api/articles")
public ResponseEntity<Article> creer(@RequestBody Article article) {
Article sauvegarde = articleRepository.save(article);
URI location = URI.create("/api/articles/" + sauvegarde.getId());
return ResponseEntity.created(location).body(sauvegarde);
}
DTO : éviter d'exposer l'entité directement
Il est déconseillé d'exposer directement l'entité JPA dans l'API. Un DTO (Data Transfer Object) est une classe simple qui expose uniquement les données nécessaires :
// DTO pour exposer un article dans l'API
public class ArticleDto {
private Long id;
private String titre;
private String contenu;
private String auteurEmail; // Seulement l'email, pas tout l'objet Utilisateur
// Constructeur depuis l'entité
public ArticleDto(Article article) {
this.id = article.getId();
this.titre = article.getTitre();
this.contenu = article.getContenu();
if (article.getAuteur() != null) {
this.auteurEmail = article.getAuteur().getEmail();
}
}
// getters (pas de setters nécessaires pour la lecture)
public Long getId() { return id; }
public String getTitre() { return titre; }
public String getContenu() { return contenu; }
public String getAuteurEmail() { return auteurEmail; }
}
Si vous retournez directement une entité JPA avec des relations @ManyToOne ou @OneToMany, Jackson peut provoquer des boucles infinies (Article → Utilisateur → List d'Article → Article → ...).
Les DTOs évitent ce problème en contrôlant exactement ce qui est sérialisé.
@PathVariable et @RequestParam dans les API
// Récupérer un article par son id dans l'URL /api/articles/42
@GetMapping("/api/articles/{id}")
public ResponseEntity<ArticleDto> detail(@PathVariable Long id) {
return articleRepository.findById(id)
.map(ArticleDto::new)
.map(ResponseEntity::ok)
.orElse(ResponseEntity.notFound().build());
}
// Recherche avec paramètre de requête /api/articles?mot=java
@GetMapping("/api/articles")
public List<ArticleDto> rechercher(@RequestParam(required = false) String mot) {
if (mot != null && !mot.isBlank()) {
return articleRepository.findByTitreContaining(mot).stream()
.map(ArticleDto::new)
.toList();
}
return articleRepository.findAll().stream()
.map(ArticleDto::new)
.toList();
}
@CrossOrigin : autoriser les appels depuis un frontend
Par défaut, les navigateurs bloquent les requêtes AJAX vers un domaine différent (politique Same-Origin). @CrossOrigin lève cette restriction :
// Autoriser uniquement http://localhost:3000 (frontend React/Vue en dev)
@CrossOrigin(origins = "http://localhost:3000")
@RestController
@RequestMapping("/api")
public class ArticleApiController {
// ...
}
// Autoriser tous les origines (pratique pour les tests, à éviter en production)
@CrossOrigin(origins = "*")
Tester une API REST avec curl
# GET - liste des articles
curl http://localhost:8080/api/articles
# GET - détail d'un article
curl http://localhost:8080/api/articles/1
# POST - créer un article (JSON dans le corps)
curl -X POST http://localhost:8080/api/articles \
-H "Content-Type: application/json" \
-d '{"titre":"Mon article","contenu":"Contenu de test"}'
# DELETE - supprimer un article
curl -X DELETE http://localhost:8080/api/articles/1
Exemple pratique
API REST CRUD complète pour Article :
package org.joliciel.monblog.controller;
import jakarta.persistence.EntityNotFoundException;
import org.joliciel.monblog.dto.ArticleDto;
import org.joliciel.monblog.entity.Article;
import org.joliciel.monblog.repository.ArticleRepository;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
import java.net.URI;
import java.util.List;
@RestController
@RequestMapping("/api/articles")
@CrossOrigin(origins = "http://localhost:3000")
public class ArticleApiController {
private final ArticleRepository articleRepository;
public ArticleApiController(ArticleRepository articleRepository) {
this.articleRepository = articleRepository;
}
// GET /api/articles → liste de tous les articles
@GetMapping
public List<ArticleDto> listerTous() {
return articleRepository.findAll().stream()
.map(ArticleDto::new)
.toList();
}
// GET /api/articles/42 → détail d'un article
@GetMapping("/{id}")
public ResponseEntity<ArticleDto> detail(@PathVariable Long id) {
return articleRepository.findById(id)
.map(ArticleDto::new)
.map(ResponseEntity::ok)
.orElse(ResponseEntity.notFound().build());
}
// POST /api/articles → créer un article
@PostMapping
public ResponseEntity<ArticleDto> creer(@RequestBody Article article) {
Article sauvegarde = articleRepository.save(article);
URI location = URI.create("/api/articles/" + sauvegarde.getId());
return ResponseEntity.created(location).body(new ArticleDto(sauvegarde));
}
// PUT /api/articles/42 → mettre à jour un article
@PutMapping("/{id}")
public ResponseEntity<ArticleDto> mettreAJour(@PathVariable Long id,
@RequestBody Article article) {
if (!articleRepository.existsById(id)) {
return ResponseEntity.notFound().build();
}
article.setId(id);
Article mis_a_jour = articleRepository.save(article);
return ResponseEntity.ok(new ArticleDto(mis_a_jour));
}
// DELETE /api/articles/42 → supprimer un article
@DeleteMapping("/{id}")
public ResponseEntity<Void> supprimer(@PathVariable Long id) {
if (!articleRepository.existsById(id)) {
return ResponseEntity.notFound().build();
}
articleRepository.deleteById(id);
return ResponseEntity.noContent().build();
}
}
Test de mémorisation/compréhension
TP pour réfléchir et résoudre des problèmes
Dans ce TP, vous allez créer une API REST CRUD pour les articles de MonBlog.
Étape 1 — Créer le DTO ArticleDto
Créez une classe ArticleDto dans le package dto qui expose id, titre et contenu.
Un DTO de lecture n'a pas besoin de setters. Une fois créé depuis l'entité, son contenu ne change plus. Ne déclarez que les getters. Cela rend le DTO plus sûr (impossible de modifier accidentellement les données après création) et plus lisible (on sait clairement que c'est un objet de sortie uniquement).
Étape 2 — Créer le RestController avec la méthode GET liste
ArticleDto::new est une référence de constructeur, équivalente à article -> new ArticleDto(article). Cette syntaxe est plus concise et lisible. Elle est idiomatique en Java moderne et à préférer dans les opérations de Stream.
Étape 3 — Méthode POST avec ResponseEntity
Retournez toujours 201 Created (et non 200 OK) pour une création réussie. Ajoutez l'en-tête Location avec l'URL de la ressource créée. Cela respecte les conventions REST et permet au client (application mobile, JavaScript...) de savoir exactement où trouver la nouvelle ressource sans la chercher.