Aller au contenu
Retour au blog
Tutoriel 15 octobre 2026 10 min de lecture

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.

Tutoriel Spring Boot React : créer une application full stack pas à pas

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 :

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 ?

OutilVersion utilisée iciVérifier avec
JDK21 (17 fonctionne aussi avec Spring Boot 3)java -version
Node.jsUne version LTS actuellenode -v
Outil de buildMaven wrapper (généré)./mvnw -v
ÉditeurIntelliJ 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 suivanteCe qu'il faut ajouter
Vraie base de donnéesAjouter 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
CouchesDé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
ErreursUn @RestControllerAdvice qui renvoie des erreurs JSON cohérentes
Tests@WebMvcTest pour les contrôleurs, @DataJpaTest pour les repositories, Vitest côté React
DéploiementUne 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 WebConfig ne correspond pas exactement à l'URL de React (protocole et port compris).
  • ERR_CONNECTION_REFUSED : l'API n'est pas lancée, ou VITE_API_URL est incorrecte. Relancez npm run dev après avoir modifié un fichier .env.
  • 400 Bad Request sur le POST : en-tête Content-Type: application/json manquant ou titre vide.
  • javax.persistence introuvable : code écrit pour Spring Boot 2 ; utilisez jakarta.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.

Spring BootReactViteREST APIJavaTypeScriptCORS
Anass Essadikine

À propos de l'auteur

Anass Essadikine

Data & AI Engineer et ingénieur logiciel basé à Casablanca, au Maroc. Je construis des plateformes data, des tableaux de bord Power BI, des assistants RAG et des applications Next.js / Spring Boot.

Projets liés

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.

Continuer la lecture

Tous les articles

Besoin de ces compétences ?

Je suis ouvert aux missions freelance, aux stages et aux postes à temps plein. Discutons.