Retour
Symfony et React pour des applications web modernes

Symfony et React pour des applications web modernes

Symfony / React

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.

CoucheTechnologieResponsabilité
FrontendReact + ViteComposants, état, navigation, affichage
CommunicationHTTP / JSONRequêtes, réponses, authentification
BackendSymfonyLogique métier, BDD, sécurité, API
Architecture Symfony React : séparation frontend et backend via une API REST

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 classiqueAvec Symfony + React séparés
Templates spaghetti Twig + JSInterface React claire, composants isolés
Logique métier dans le JSTout reste côté Symfony, testable en PHP
Sécurité gérée côté clientSymfony gère l’auth, React ne voit que ce qu’on lui donne
Déploiements couplésFrontend et backend déployés indépendamment
Difficile à passer en mobile appReact 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.