Symfony et React pour des applications web modernes
24 mai 2026
Introduction
Symfony et React répondent à des problèmes différents. Symfony est très bon pour structurer la logique serveur, gérer les données, les droits, les formulaires complexes. React est très bon pour construire des interfaces réactives, gérer l’état côté client, et offrir une expérience utilisateur fluide.
Mis ensemble, ils forment un couple cohérent — à condition que chacun reste dans son rôle.
1. Le principe de séparation
La décision architecturale principale est simple : Symfony expose des données via une API, React les consomme.
| Couche | Technologie | Responsabilité |
|---|---|---|
| Frontend | React + Vite | Composants, état, navigation, affichage |
| Communication | HTTP / JSON | Requêtes, réponses, authentification |
| Backend | Symfony | Logique métier, BDD, sécurité, API |
Ni React ne doit toucher la base de données, ni Symfony ne doit générer du JSX. Chacun fait son travail. La frontière, c’est l’API.
2. Symfony côté backend
Symfony expose ses données via des controllers qui retournent du JSON. Pas de templates Twig dans ce cas — on travaille en mode API pure.
// src/Controller/Api/PostController.php
#[Route('/api/posts', methods: ['GET'])]
public function index(PostRepository $repo): JsonResponse
{
$posts = $repo->findAll();
return $this->json($posts, 200, [], ['groups' => ['post:read']]);
}
#[Route('/api/posts/{id}', methods: ['GET'])]
public function show(Post $post): JsonResponse
{
return $this->json($post, 200, [], ['groups' => ['post:read']]);
}
#[Route('/api/posts', methods: ['POST'])]
public function create(Request $request, PostService $service): JsonResponse
{
$post = $service->create($request->toArray());
return $this->json($post, 201, [], ['groups' => ['post:read']]);
}
Le Serializer de Symfony gère la sérialisation via des groupes, ce qui permet de contrôler précisément ce qui est exposé dans l’API.
// src/Entity/Post.php
use Symfony\Component\Serializer\Annotation\Groups;
class Post
{
#[Groups(['post:read'])]
private int $id;
#[Groups(['post:read', 'post:write'])]
private string $title;
#[Groups(['post:read'])]
private \DateTimeImmutable $createdAt;
// Le contenu interne n'est pas exposé par défaut
private string $internalNotes;
}
3. CORS et JWT
Deux points techniques à régler dès le départ quand Symfony et React tournent sur des origines différentes.
CORS — le navigateur bloque les requêtes cross-origin par défaut. Le bundle nelmio/cors-bundle règle ça proprement :
composer require nelmio/cors-bundle
# config/packages/nelmio_cors.yaml
nelmio_cors:
defaults:
origin_regex: true
allow_origin: ['%env(CORS_ALLOW_ORIGIN)%']
allow_methods: ['GET', 'OPTIONS', 'POST', 'PUT', 'PATCH', 'DELETE']
allow_headers: ['Content-Type', 'Authorization']
max_age: 3600
paths:
'^/api/': ~
JWT — pour l’authentification sans session côté serveur, lexik/jwt-authentication-bundle est le standard :
composer require lexik/jwt-authentication-bundle
Le flux est simple : React envoie les credentials → Symfony retourne un token JWT → React stocke le token et le joint à chaque requête suivante dans le header Authorization: Bearer ....
4. React côté frontend
React consomme l’API avec fetch ou axios. Avec Vite comme bundler, le setup est rapide :
npm create vite@latest frontend -- --template react-ts
cd frontend && npm install
Un hook pour appeler l’API Symfony :
// src/hooks/usePosts.ts
import { useState, useEffect } from 'react';
interface Post {
id: number;
title: string;
createdAt: string;
}
export function usePosts() {
const [posts, setPosts] = useState<Post[]>([]);
const [loading, setLoading] = useState(true);
useEffect(() => {
fetch('/api/posts', {
headers: {
Authorization: `Bearer ${localStorage.getItem('token')}`,
},
})
.then((res) => res.json())
.then((data) => {
setPosts(data);
setLoading(false);
});
}, []);
return { posts, loading };
}
Et un composant qui l’utilise :
// src/components/PostList.tsx
import { usePosts } from '../hooks/usePosts';
export function PostList() {
const { posts, loading } = usePosts();
if (loading) return <p>Chargement...</p>;
return (
<ul>
{posts.map((post) => (
<li key={post.id}>{post.title}</li>
))}
</ul>
);
}
5. Ce qu’on évite avec cette architecture
| Problème classique | Avec Symfony + React séparés |
|---|---|
| Templates spaghetti Twig + JS | Interface React claire, composants isolés |
| Logique métier dans le JS | Tout reste côté Symfony, testable en PHP |
| Sécurité gérée côté client | Symfony gère l’auth, React ne voit que ce qu’on lui donne |
| Déploiements couplés | Frontend et backend déployés indépendamment |
| Difficile à passer en mobile app | React Native peut consommer la même API |
6. Quand ce n’est pas la bonne approche
Cette architecture a du sens pour les applications avec beaucoup d’interactivité côté client. Pour un site éditorial, un blog, ou une application avec peu de dynamisme, le coût est disproportionné.
Dans ces cas-là, Twig avec quelques sprinkles de Stimulus ou Turbo fait le travail avec beaucoup moins de complexité. Ne pas sur-architecturer.
Conclusion
Symfony + React, c’est une architecture claire quand on respecte le contrat : Symfony gère les données et la logique, React gère l’affichage et l’interaction. L’API est la frontière.
Cette séparation rend le code plus testable, les équipes plus autonomes, et l’application plus facile à faire évoluer. Elle a aussi un coût en complexité initiale — à peser selon la taille et la durée de vie du projet.