Tutoriel Spring Boot React : créer une application full stack pas à pas
Un tutoriel complet et exécutable : une API REST Spring Boot 3 avec JPA et validation, une configuration CORS propre et un front-end React + TypeScript créé avec Vite qui liste, crée, modifie et supprime des données.

Pour créer une application full stack avec Spring Boot et React, on construit une API REST Spring Boot 3 qui expose du JSON sous /api, on autorise l'origine de développement de React via CORS, puis on appelle ces endpoints depuis une application React créée avec Vite. Ce tutoriel construit de bout en bout un petit gestionnaire de tâches : entité, repository, contrôleur, CORS, puis l'écran React qui liste, ajoute, coche et supprime des tâches.
Pourquoi associer Spring Boot et React ?
Spring Boot apporte un back-end structuré et fortement typé, avec un outillage mature pour l'accès aux données, la validation et la sécurité. React apporte une interface à base de composants, simple à rendre interactive. Le duo est très répandu dans les projets d'entreprise, et c'est le stack de plusieurs projets réels de ce portfolio :
- la plateforme de gestion du logement universitaire (FSBM) repose sur une API REST Spring Boot avec MySQL et un front-end React + Tailwind, avec JWT pour l'accès par rôle ;
- la plateforme D&A Studio utilise des API REST Spring Boot (Java 21) sur PostgreSQL/PostGIS et une interface React avec cartographie.
Le tutoriel ci-dessous est volontairement plus petit, mais la structure (entité, repository, contrôleur, couche API côté React) est celle que l'on étend dans les applications plus grandes.
De quoi avez-vous besoin avant de commencer ?
| Outil | Version utilisée ici | Vérifier avec |
|---|---|---|
| JDK | 21 (17 fonctionne aussi avec Spring Boot 3) | java -version |
| Node.js | Une version LTS actuelle | node -v |
| Outil de build | Maven wrapper (généré) | ./mvnw -v |
| Éditeur | IntelliJ IDEA, VS Code ou Eclipse | — |
Le back-end utilise une base H2 en mémoire : tout tourne sans installer de serveur de base de données. Passer à MySQL ou PostgreSQL ensuite n'est qu'une question de configuration.
Étape 1 — Comment créer le projet Spring Boot ?
Le générer avec Spring Initializr
Rendez-vous sur start.spring.io et choisissez :
- Project : Maven, Language : Java, Spring Boot : la dernière version 3.x ;
- Group :
com.example, Artifact :tasks, Java : 21 ; - Dépendances : Spring Web, Spring Data JPA, Validation, H2 Database.
Téléchargez, décompressez et ouvrez le projet. La classe principale est com.example.tasks.TasksApplication.
Créer l'entité
// src/main/java/com/example/tasks/Task.java
package com.example.tasks;
import jakarta.persistence.Entity;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
@Entity
public class Task {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String title;
private boolean done;
protected Task() {
// constructeur requis par JPA
}
public Task(String title) {
this.title = title;
}
public Long getId() { return id; }
public String getTitle() { return title; }
public void setTitle(String title) { this.title = title; }
public boolean isDone() { return done; }
public void setDone(boolean done) { this.done = done; }
}
Spring Boot 3 utilise le package jakarta.persistence, et non plus javax.persistence. Jackson sérialise cet objet en {"id":1,"title":"...","done":false}.
Créer le repository
// src/main/java/com/example/tasks/TaskRepository.java
package com.example.tasks;
import org.springframework.data.jpa.repository.JpaRepository;
public interface TaskRepository extends JpaRepository<Task, Long> {
}
Spring Data génère l'implémentation : findAll, findById, save, deleteById et d'autres, sans écrire de SQL.
Étape 2 — Comment écrire le contrôleur REST ?
Utilisez un petit record de requête en entrée plutôt que de lier directement l'entité. Le client ne peut pas imposer des champs comme id, et la validation devient explicite.
// src/main/java/com/example/tasks/CreateTaskRequest.java
package com.example.tasks;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.Size;
public record CreateTaskRequest(@NotBlank @Size(max = 200) String title) {
}
// src/main/java/com/example/tasks/TaskController.java
package com.example.tasks;
import jakarta.validation.Valid;
import java.util.List;
import org.springframework.http.HttpStatus;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.server.ResponseStatusException;
@RestController
@RequestMapping("/api/tasks")
public class TaskController {
private final TaskRepository repository;
public TaskController(TaskRepository repository) {
this.repository = repository;
}
@GetMapping
public List<Task> findAll() {
return repository.findAll();
}
@PostMapping
@ResponseStatus(HttpStatus.CREATED)
public Task create(@Valid @RequestBody CreateTaskRequest request) {
return repository.save(new Task(request.title().trim()));
}
@PatchMapping("/{id}/toggle")
public Task toggle(@PathVariable Long id) {
Task task = repository.findById(id)
.orElseThrow(() -> new ResponseStatusException(HttpStatus.NOT_FOUND, "Task not found"));
task.setDone(!task.isDone());
return repository.save(task);
}
@DeleteMapping("/{id}")
@ResponseStatus(HttpStatus.NO_CONTENT)
public void delete(@PathVariable Long id) {
if (!repository.existsById(id)) {
throw new ResponseStatusException(HttpStatus.NOT_FOUND, "Task not found");
}
repository.deleteById(id);
}
}
L'injection par constructeur n'a pas besoin de @Autowired quand la classe n'a qu'un constructeur. Un titre vide ou absent renvoie 400 Bad Request grâce à @Valid.
Insérer quelques lignes au démarrage
Ajoutez un bean CommandLineRunner dans la classe principale pour que la liste ne soit pas vide au premier lancement :
// src/main/java/com/example/tasks/TasksApplication.java
package com.example.tasks;
import org.springframework.boot.CommandLineRunner;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.annotation.Bean;
@SpringBootApplication
public class TasksApplication {
public static void main(String[] args) {
SpringApplication.run(TasksApplication.class, args);
}
@Bean
CommandLineRunner seed(TaskRepository repository) {
return args -> {
if (repository.count() == 0) {
repository.save(new Task("Lire la documentation Spring Boot"));
repository.save(new Task("Construire le front-end React"));
}
};
}
}
Étape 3 — Comment configurer CORS pour React ?
En développement, React tourne sur http://localhost:5173 et l'API sur http://localhost:8080. Ce sont deux origines différentes : le navigateur bloque les appels tant que l'API ne les autorise pas. Configurez CORS une seule fois, globalement :
// src/main/java/com/example/tasks/WebConfig.java
package com.example.tasks;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.CorsRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
@Configuration
public class WebConfig implements WebMvcConfigurer {
@Value("${app.cors.allowed-origins:http://localhost:5173}")
private String[] allowedOrigins;
@Override
public void addCorsMappings(CorsRegistry registry) {
registry.addMapping("/api/**")
.allowedOrigins(allowedOrigins)
.allowedMethods("GET", "POST", "PATCH", "DELETE");
}
}
Les origines autorisées viennent de la propriété app.cors.allowed-origins (séparées par des virgules), avec l'URL de Vite par défaut. En production, mettez-y votre vrai domaine au lieu de *. Si vous ajoutez Spring Security plus tard, activez aussi CORS dans la chaîne de filtres avec http.cors(Customizer.withDefaults()), sinon les filtres de sécurité rejettent les requêtes preflight.
Lancer et tester l'API
./mvnw spring-boot:run
curl http://localhost:8080/api/tasks
curl -X POST http://localhost:8080/api/tasks \
-H "Content-Type: application/json" \
-d '{"title":"Écrire des tests"}'
Sous Windows, utilisez mvnw.cmd spring-boot:run. Le premier appel renvoie les deux tâches insérées, en JSON.
Étape 4 — Comment créer le front-end React avec Vite ?
npm create vite@latest tasks-ui -- --template react-ts
cd tasks-ui
npm install
Créez un fichier .env.development à la racine de tasks-ui :
VITE_API_URL=http://localhost:8080
Écrire une petite couche API
Regroupez tous les appels HTTP dans un seul fichier. Les composants restent simples, et changer l'URL ou ajouter un en-tête d'authentification se fait à un seul endroit.
// src/api.ts
export type Task = { id: number; title: string; done: boolean };
const API_URL = import.meta.env.VITE_API_URL ?? "http://localhost:8080";
async function request<T>(path: string, init?: RequestInit): Promise<T> {
const res = await fetch(`${API_URL}/api${path}`, {
headers: { "Content-Type": "application/json" },
...init,
});
if (!res.ok) {
throw new Error(`HTTP ${res.status}`);
}
return (res.status === 204 ? undefined : await res.json()) as T;
}
export const getTasks = () => request<Task[]>("/tasks");
export const createTask = (title: string) =>
request<Task>("/tasks", { method: "POST", body: JSON.stringify({ title }) });
export const toggleTask = (id: number) =>
request<Task>(`/tasks/${id}/toggle`, { method: "PATCH" });
export const deleteTask = (id: number) =>
request<void>(`/tasks/${id}`, { method: "DELETE" });
Construire le composant
Remplacez le contenu de src/App.tsx :
// src/App.tsx
import { useEffect, useState, type FormEvent } from "react";
import { createTask, deleteTask, getTasks, toggleTask, type Task } from "./api";
export default function App() {
const [tasks, setTasks] = useState<Task[]>([]);
const [title, setTitle] = useState("");
const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
useEffect(() => {
getTasks()
.then(setTasks)
.catch((err: Error) => setError(err.message))
.finally(() => setLoading(false));
}, []);
async function handleSubmit(event: FormEvent<HTMLFormElement>) {
event.preventDefault();
if (!title.trim()) return;
try {
const created = await createTask(title.trim());
setTasks((prev) => [...prev, created]);
setTitle("");
setError(null);
} catch (err) {
setError((err as Error).message);
}
}
async function handleToggle(id: number) {
try {
const updated = await toggleTask(id);
setTasks((prev) => prev.map((t) => (t.id === id ? updated : t)));
} catch (err) {
setError((err as Error).message);
}
}
async function handleDelete(id: number) {
try {
await deleteTask(id);
setTasks((prev) => prev.filter((t) => t.id !== id));
} catch (err) {
setError((err as Error).message);
}
}
return (
<main style={{ maxWidth: 480, margin: "2rem auto", fontFamily: "sans-serif" }}>
<h1>Tâches</h1>
<form onSubmit={handleSubmit}>
<input
value={title}
onChange={(e) => setTitle(e.target.value)}
placeholder="Nouvelle tâche"
aria-label="Nouvelle tâche"
/>
<button type="submit">Ajouter</button>
</form>
{error && <p role="alert">Erreur : {error}</p>}
{loading && <p>Chargement...</p>}
{!loading && tasks.length === 0 && <p>Aucune tâche pour l'instant.</p>}
<ul>
{tasks.map((task) => (
<li key={task.id}>
<label>
<input
type="checkbox"
checked={task.done}
onChange={() => handleToggle(task.id)}
/>
{task.title}
</label>
<button onClick={() => handleDelete(task.id)}>Supprimer</button>
</li>
))}
</ul>
</main>
);
}
Lancez npm run dev et ouvrez http://localhost:5173. Avec l'API démarrée, les tâches insérées s'affichent et vous pouvez en ajouter, en cocher et en supprimer. En développement, le Strict Mode de React exécute les effets deux fois : voir deux requêtes GET dans l'onglet réseau est normal.
Étape 5 — Comment passer du tutoriel à un vrai projet ?
| Étape suivante | Ce qu'il faut ajouter |
|---|---|
| Vraie base de données | Ajouter le driver MySQL ou PostgreSQL et renseigner spring.datasource.url, l'utilisateur et le mot de passe ; gérer les migrations avec Flyway ou Liquibase |
| Couches | Déplacer la logique dans un @Service et renvoyer des DTO plutôt que des entités |
| Sécurité | Spring Security avec JWT et rôles, comme dans le projet de logement FSBM |
| Erreurs | Un @RestControllerAdvice qui renvoie des erreurs JSON cohérentes |
| Tests | @WebMvcTest pour les contrôleurs, @DataJpaTest pour les repositories, Vitest côté React |
| Déploiement | Une image Docker par côté, ou React compilé et servi depuis le dossier static de Spring Boot ; un pipeline CI/CD comme dans l'article CI/CD Kubernetes |
Erreurs fréquentes et solutions
- Erreur CORS dans la console du navigateur : l'origine dans
WebConfigne correspond pas exactement à l'URL de React (protocole et port compris). ERR_CONNECTION_REFUSED: l'API n'est pas lancée, ouVITE_API_URLest incorrecte. Relanceznpm run devaprès avoir modifié un fichier.env.400 Bad Requestsur le POST : en-têteContent-Type: application/jsonmanquant ou titre vide.javax.persistenceintrouvable : code écrit pour Spring Boot 2 ; utilisezjakarta.persistence.
Pour approfondir le côté back-end, le guide officiel Spring « Building a RESTful Web Service » est une bonne référence.
Conclusion
Une application Spring Boot + React repose sur trois briques : une API REST propre, une configuration CORS correcte et une fine couche API côté front-end. Une fois ce squelette fonctionnel, ajouter la sécurité, une vraie base de données et le déploiement se fait progressivement. Pour situer ce stack dans un ensemble de compétences plus large, lisez la roadmap pour devenir développeur full stack ou consultez le profil de développeur full stack.
Projets liés
Génie logicielProjet académique · 2024Gestion du logement universitaire (FSBM)
Une plateforme sécurisée pour gérer les demandes de logement étudiant, les affectations et les paiements de bout en bout.
IA & AutomatisationStage · 2026D&A Studio — Plateforme Data & IA (PaaS)
Une plateforme qui réunit données de traçage géospatial, assistant IA sur les documents de l'entreprise, détection d'anomalies et tableaux de bord BI.
Questions fréquentes
Exposez des endpoints JSON avec un @RestController, autorisez l'origine de React via une configuration CORS, puis appelez ces endpoints depuis React avec fetch ou Axios. Gardez l'URL de base de l'API dans une variable d'environnement Vite comme VITE_API_URL.



