</>
{ }
React · Symfony · API Platform · JWT · React Query
React + Symfony Fullstack-App
mit API Platform

Symfony ist das robusteste PHP-Framework für komplexe Backend-Logik. React ist das mächtigste UI-Framework für dynamische Frontends. API Platform verbindet beides durch automatisch generierte REST- und GraphQL-APIs — mit Validierung, Serialisierung und OpenAPI-Dokumentation out of the box. Dieser Artikel zeigt, wie man die beiden Welten produktionsreif zusammenführt.

20 Min. Lesezeit API Platform · JWT Auth · React Query · OpenAPI · CORS React 18 · Symfony 7 · API Platform 3 · PHP 8.4

1. Warum React und Symfony zusammen

Symfony und React bedienen unterschiedliche Stärken in der Fullstack-Entwicklung. Symfony glänzt mit komplexer Domain-Logik, Dependency Injection, Command-Bus-Patterns und einem ausgereiften Ökosystem für Datenbankoperationen, Queue-Verarbeitung und Security. React übernimmt die interaktive UI-Schicht mit reaktivem State, flüssigen Transitionen und einem riesigen Ökosystem für UI-Komponenten. Die Verbindung zwischen beiden ist eine HTTP-API — und hier setzt API Platform an.

Der entscheidende Vorteil dieser Kombination gegenüber einem Next.js-Fullstack-Ansatz: Symfony bietet erstklassige PHP-Tools für alles, was außerhalb der HTTP-Anfrage passiert — Cron-Jobs, Console-Commands, Message-Queue-Handler, komplexe Datenbankmigrationen. React mit einem separaten Backend entkoppelt Frontend- und Backend-Deployment vollständig, was bei Teams mit unterschiedlichen Spezialisierungen erhebliche Organisationsvorteile bringt. API Platform schließt die Lücke, indem es Symfony-Entities automatisch in dokumentierte, typsichere APIs verwandelt.

2. API Platform: REST-API in Minuten

API Platform ist ein Symfony-Bundle, das PHP-Klassen mit dem Attribut #[ApiResource] in vollständige REST-APIs verwandelt. Es generiert automatisch CRUD-Endpunkte, Validierung über Symfony-Constraints, Serialisierung über Symfony-Serializer und eine interaktive OpenAPI-Dokumentation (Swagger UI). Die API folgt dem JSON:API- oder JSON-LD-Standard, kann aber für einfaches JSON konfiguriert werden. Was früher hunderte Zeilen Controller-Code erforderte, sind jetzt wenige Zeilen PHP-Attribut-Konfiguration.

Die wichtigste Konfigurationsoption für React-Frontend-Entwicklung ist die Normalisierungsgruppe. Mit normalizationContext: ['groups' => ['product:read']] und denormalizationContext: ['groups' => ['product:write']] steuert man exakt, welche Felder die API zurückgibt und welche sie akzeptiert — ohne separate DTO-Klassen erstellen zu müssen. Das verhindert sowohl Over-Fetching als auch das versehentliche Exponieren interner Felder. Jede Property erhält zusätzlich ein #[Groups]-Attribut, das festlegt, in welchen Kontexten sie sichtbar ist.


<?php
// src/Entity/Product.php — API Platform resource with serialization groups
declare(strict_types=1);

namespace App\Entity;

use ApiPlatform\Metadata\ApiResource;
use ApiPlatform\Metadata\Get;
use ApiPlatform\Metadata\GetCollection;
use ApiPlatform\Metadata\Post;
use ApiPlatform\Metadata\Put;
use ApiPlatform\Metadata\Delete;
use Doctrine\ORM\Mapping as ORM;
use Symfony\Component\Serializer\Annotation\Groups;
use Symfony\Component\Validator\Constraints as Assert;

#[ApiResource(
    operations: [
        new Get(normalizationContext: ['groups' => ['product:read']]),
        new GetCollection(normalizationContext: ['groups' => ['product:read']]),
        new Post(
            normalizationContext: ['groups' => ['product:read']],
            denormalizationContext: ['groups' => ['product:write']],
            security: "is_granted('ROLE_ADMIN')"
        ),
        new Put(denormalizationContext: ['groups' => ['product:write']]),
        new Delete(security: "is_granted('ROLE_ADMIN')"),
    ]
)]
#[ORM\Entity]
class Product
{
    #[ORM\Id, ORM\GeneratedValue, ORM\Column]
    #[Groups(['product:read'])]
    private ?int $id = null;

    #[ORM\Column(length: 255)]
    #[Groups(['product:read', 'product:write'])]
    #[Assert\NotBlank]
    #[Assert\Length(max: 255)]
    private string $name = '';

    #[ORM\Column(type: 'decimal', precision: 10, scale: 2)]
    #[Groups(['product:read', 'product:write'])]
    #[Assert\Positive]
    private string $price = '0.00';
}

3. CORS korrekt konfigurieren

Der häufigste erste Fehler beim Verbinden von React (localhost:3000) mit einem Symfony-Backend (localhost:8000) ist ein CORS-Fehler im Browser. CORS (Cross-Origin Resource Sharing) ist ein Browser-Sicherheitsmechanismus, der HTTP-Anfragen zwischen verschiedenen Origins blockiert, solange der Server nicht explizit zustimmt. Symfony löst das über das Bundle nelmio/cors-bundle. Nach der Installation konfiguriert man in config/packages/nelmio_cors.yaml, welche Origins, Methoden und Header erlaubt sind.

Ein häufiger Fehler: CORS nur für GET-Anfragen konfigurieren. POST, PUT, DELETE und PATCH lösen einen Preflight-Request aus (OPTIONS-Methode), den der Server ebenfalls beantworten muss. In der Entwicklung empfiehlt sich eine großzügige CORS-Konfiguration (allow_origin: ['*']), in der Produktion hingegen eine strikte Liste aller erlaubten Frontend-Origins. JWT-Tokens werden als Authorization: Bearer-Header gesendet — dieser muss in allow_headers explizit aufgeführt sein, sonst blockiert der Browser auch authentifizierte Anfragen.

4. JWT-Authentifizierung mit LexikJWTBundle

JSON Web Tokens sind der Standard für zustandslose Authentifizierung in React-Symfony-Fullstack-Anwendungen. Das Bundle lexik/jwt-authentication-bundle generiert und validiert JWTs und integriert sich nahtlos in Symfonie's Security-Firewall. Der Ablauf: Das React-Frontend sendet E-Mail und Passwort an /api/login_check, erhält einen JWT zurück und speichert ihn sicher. Alle nachfolgenden API-Anfragen senden den Token im Authorization: Bearer-Header. Symfony validiert den Token automatisch, extrahiert den User und stellt ihn in der Security-Infrastruktur bereit.

Für die Token-Speicherung im React-Frontend gibt es zwei Optionen: localStorage (einfach, aber anfällig für XSS) und httpOnly-Cookies (sicherer gegen XSS, aber erfordert CSRF-Schutz). Für React-Anwendungen mit sorgfältiger XSS-Prevention ist sessionStorage oft ein guter Kompromiss — der Token wird bei Tab-Schließen gelöscht, was die Session-Länge begrenzt. Ein Refresh-Token-Mechanismus mit gesdinet/jwt-refresh-token-bundle verlängert Sessions, ohne den Nutzer zum erneuten Login zu zwingen.


// hooks/useAuth.tsx — JWT auth with React Query and secure storage
import { useMutation, useQueryClient } from '@tanstack/react-query';

interface LoginCredentials { email: string; password: string; }
interface AuthResponse { token: string; }

// Auth client — send credentials, receive JWT
const loginRequest = async (credentials: LoginCredentials): Promise<AuthResponse> => {
  const res = await fetch('/api/login_check', {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify(credentials),
  });
  if (!res.ok) throw new Error('Login fehlgeschlagen');
  return res.json();
};

export const useAuth = () => {
  const queryClient = useQueryClient();

  const loginMutation = useMutation({
    mutationFn: loginRequest,
    onSuccess: ({ token }) => {
      // Store token in memory (most secure) or sessionStorage
      sessionStorage.setItem('jwt_token', token);
      // Invalidate all cached queries — user context changed
      queryClient.invalidateQueries();
    },
  });

  const logout = () => {
    sessionStorage.removeItem('jwt_token');
    queryClient.clear();
  };

  return { login: loginMutation.mutate, logout, isLoading: loginMutation.isPending };
};

// Axios interceptor — attach JWT to every request automatically
import axios from 'axios';

axios.interceptors.request.use(config => {
  const token = sessionStorage.getItem('jwt_token');
  if (token) config.headers.Authorization = `Bearer ${token}`;
  return config;
});

5. Typsicherer API-Client aus OpenAPI generieren

API Platform generiert automatisch eine OpenAPI 3.0-Spezifikation für alle #[ApiResource]-Klassen. Diese Spezifikation ist unter /api/docs.json verfügbar und enthält alle Endpunkte, Request-Bodies, Response-Schemas und mögliche Fehlercodes. Der nächste Schritt: aus dieser Spezifikation automatisch einen TypeScript-Client generieren. Das Tool openapi-typescript konvertiert die OpenAPI-Spec in TypeScript-Typen, und openapi-fetch generiert einen vollständig typisierten Fetch-Client.

Der Workflow ist dann: Symfony-Entity ändern, npm run generate-api-types ausführen, und alle Stellen im React-Code, die inkompatibel mit den neuen Typen sind, werden sofort durch TypeScript-Fehler sichtbar — noch bevor ein einziger Test läuft. Das ist der größte Vorteil dieser Kombination gegenüber manuell geschriebenen API-Clients: Die Typen sind immer synchron mit dem tatsächlichen Backend, und Breaking Changes in der API werden zur Compile-Zeit entdeckt, nicht zur Laufzeit.

6. React Query für Datenfetching und Caching

React Query (TanStack Query) ist die empfohlene Datenfetching-Bibliothek für React-Symfony-Fullstack-Anwendungen. Sie verwaltet den gesamten Lifecycle von API-Anfragen: Loading-State, Success-State, Error-State, Caching, automatisches Re-Fetching nach Focus-Events und optimistic Updates. Im Vergleich zu manuellem Fetching mit useEffect und useState eliminiert React Query tausende Zeilen Boilerplate und gibt Caching-Mechanismen, die viele selbstgebaute Lösungen übertreffen.

Für das Zusammenspiel mit API Platform ist die queryKey-Strategie entscheidend. Jeder API-Endpunkt bekommt einen konsistenten Query-Key — zum Beispiel ['products', { page, filter }] für eine paginierte Produktliste. Wenn eine Mutation erfolgreich abgeschlossen wird, invalidiert man alle Queries mit dem Präfix ['products']. React Query refetcht dann automatisch alle betroffenen Queries — der Cache bleibt konsistent, ohne dass man manuell State synchronisieren muss. Die Integration mit API Platform's Paginierung, Filtern und Sortieren über Query-Parameter ist direkt und benötigt keine zusätzliche Konfiguration.

7. Formulare mit React Hook Form und Validierung

React Hook Form ist die Performance-erste Bibliothek für Formularvalidierung in React. Sie registriert Inputs über Refs statt über State, was bedeutet: kein Re-Render bei jedem Tastenanschlag. Bei komplexen Formularen mit vielen Feldern ist der Performance-Unterschied zu State-basierten Ansätzen (Formik, kontrollierte Inputs) enorm messbar. Die Integration mit Symfony's Validierungsfehlern ist direkt: API Platform gibt Validierungsfehler im JSON:API-Format zurück, und man mappt diese auf die entsprechenden Formularfelder mit setError(fieldName, { message }).

Das Zusammenspiel mit Zod für Client-seitige Validierung erlaubt es, dieselben Regeln auf dem Frontend zu definieren, die Symfony auf dem Backend validiert — als zusätzliche Sicherheitsschicht und für sofortiges Feedback ohne API-Roundtrip. Mit @hookform/resolvers/zod integriert sich Zod direkt in React Hook Form. Die Kombination aus Client-Validierung (Zod), API-Validierung (Symfony Constraints) und typsicheren Formulardaten (TypeScript) schließt die Validierungs-Loop komplett.


// components/ProductForm.tsx — React Hook Form + Zod + API Platform error mapping
import { useForm } from 'react-hook-form';
import { zodResolver } from '@hookform/resolvers/zod';
import { z } from 'zod';
import { useMutation, useQueryClient } from '@tanstack/react-query';
import { apiClient } from '../lib/apiClient'; // generated OpenAPI client

// Zod schema mirrors Symfony constraints (NotBlank, Length, Positive)
const productSchema = z.object({
  name: z.string().min(1, 'Name ist erforderlich').max(255),
  price: z.number().positive('Preis muss positiv sein'),
});

type ProductFormData = z.infer<typeof productSchema>;

export const ProductForm = () => {
  const queryClient = useQueryClient();
  const { register, handleSubmit, setError, formState: { errors } } = useForm<ProductFormData>({
    resolver: zodResolver(productSchema),
  });

  const mutation = useMutation({
    mutationFn: (data: ProductFormData) => apiClient.POST('/api/products', { body: data }),
    onSuccess: () => {
      queryClient.invalidateQueries({ queryKey: ['products'] });
    },
    onError: async (error: Response) => {
      // Map API Platform validation errors to form fields
      const body = await error.json();
      body.violations?.forEach(({ propertyPath, message }: any) => {
        setError(propertyPath as keyof ProductFormData, { message });
      });
    },
  });

  return (
    <form onSubmit={handleSubmit(data => mutation.mutate(data))}>
      <input {...register('name')} placeholder="Produktname" />
      {errors.name && <span>{errors.name.message}</span>}
      <input {...register('price', { valueAsNumber: true })} type="number" step="0.01" />
      {errors.price && <span>{errors.price.message}</span>}
      <button type="submit" disabled={mutation.isPending}>Speichern</button>
    </form>
  );
};

8. Fehlerbehandlung und API-Fehlermapping

API Platform gibt Fehler in einem standardisierten Format zurück: HTTP-Status-Code plus JSON-Body mit type, title, detail und bei Validierungsfehlern ein violations-Array. Das ist ein enormer Vorteil gegenüber selbstgebauten APIs — jedes Frontend kann dasselbe Parsing-Schema nutzen. Eine zentrale Fehlerbehandlungsfunktion im React-Frontend parst dieses Format und konvertiert es in nutzbare Error-Objekte: HTTP-404 wird zu "Nicht gefunden", HTTP-422 mit Validierungsfehlern wird zu einem Mapping von Feld zu Fehlermeldung.

Für nicht-triviale Fehler — Server-Timeouts, Netzwerkausfälle, unerwartete 500er — braucht man zusätzlich eine globale Error-Boundary in React, die dem Nutzer eine sinnvolle Nachricht zeigt, statt die App komplett zum Absturz zu bringen. React Query bietet einen globalen onError-Callback auf dem QueryClient, der alle Query-Fehler zentral behandeln kann — zum Beispiel, um bei einem 401-Fehler den Nutzer automatisch auszuloggen.

9. Architekturansätze im Vergleich

Es gibt mehrere Architekturmuster für React-Symfony-Fullstack-Anwendungen. Die Wahl beeinflusst Deployment-Komplexität, Entwicklungsgeschwindigkeit und langfristige Wartbarkeit erheblich.

Architektur SEO Komplexität Empfehlung
React SPA + Symfony API Schlecht ohne SSR Gering Internes Dashboard, Admin-Tools
Next.js + Symfony API Gut (SSR/SSG) Mittel Public-facing Apps mit SEO-Anforderungen
Symfony + Twig + React Islands Sehr gut Mittel Content-Sites mit interaktiven Bereichen
API Platform + Admin (React Admin) Nicht relevant Sehr gering CRUD-heavy Admin-Panels schnell bauen
Symfony Mercure + React (Live) Mittel Hoch Realtime-Features (Chat, Live-Updates)

Für die meisten Anwendungsfälle ist die Kombination React SPA + Symfony API mit API Platform der beste Startpunkt. Sie ist einfach zu verstehen, einfach zu deployen (zwei separate Services) und gibt Teams volle Kontrolle über Frontend- und Backend-Entwicklung. Wenn SEO später wichtig wird, kann man das React-Frontend auf Next.js migrieren — das Backend bleibt unverändert. Diese evolutionäre Strategie ist besser als von Anfang an eine Komplexität zu wählen, die man noch nicht braucht.

Mironsoft

React-Frontend und Symfony-Backend als produktionsreife Fullstack-App

Fullstack-App mit React und Symfony bauen?

Wir planen und implementieren eure Fullstack-Architektur mit React, Symfony und API Platform — von der Domain-Modellierung über JWT-Auth bis zum produktionsreifen Deployment mit CI/CD.

API-Design

API Platform konfigurieren, Serialisierungsgruppen definieren, OpenAPI-Spec generieren

Auth & Security

JWT, Refresh-Tokens, Rollen-basierte Access Control und CORS-Konfiguration

Frontend-Integration

Typsichere API-Clients, React Query und Formularvalidierung mit Zod

10. Zusammenfassung

React + Symfony mit API Platform ist eine produktionsreife Fullstack-Kombination, die die Stärken beider Welten verbindet: Symfonie's robuste Backend-Infrastruktur und React's reaktive UI-Schicht. API Platform generiert REST-APIs, OpenAPI-Dokumentation und Validierung direkt aus PHP-Attributen. Der OpenAPI-generierte TypeScript-Client stellt sicher, dass Frontend-Code immer mit dem Backend-Contract synchron ist. JWT mit LexikJWTBundle implementiert zustandslose Authentifizierung, React Query verwaltet Caching und Loading-States, und React Hook Form mit Zod schließt die Validierungs-Loop zwischen Frontend und Backend.

Der iterative Aufbau dieser Architektur beginnt mit einer einzelnen #[ApiResource]-Klasse, einem generierten Client und einem einfachen React-Query-Hook. Von dort aus wächst die Anwendung organisch: mehr Entities, mehr Queries, mehr Mutations. Die Typsicherheit durch OpenAPI-generierte Clients stellt sicher, dass Wachstum keine technische Schulden aufbaut — Breaking Changes in der API sind Compile-Fehler, keine Laufzeit-Überraschungen.

React + Symfony + API Platform — Das Wichtigste auf einen Blick

API Platform Setup

#[ApiResource] auf Entity-Klassen — generiert CRUD, Validierung, OpenAPI und JSON-LD out of the box.

Typsichere Clients

openapi-typescript + openapi-fetch generieren TypeScript-Clients aus der API-Spec — Breaking Changes werden Compile-Fehler.

JWT + React Query

LexikJWTBundle für Token-Generierung, Axios-Interceptor für automatisches Anhängen, React Query für Caching und Invalidierung.

CORS konfigurieren

nelmio/cors-bundle mit allow_headers: [Authorization, Content-Type] und allow_methods: [GET, POST, PUT, PATCH, DELETE, OPTIONS].

11. FAQ: React + Symfony Fullstack mit API Platform

1Was ist API Platform?
Symfony-Bundle das #[ApiResource]-Klassen in vollständige REST-APIs mit Validierung, Serialisierung und OpenAPI-Doku verwandelt — ohne Controller-Code.
2CORS-Fehler zwischen React und Symfony lösen?
nelmio/cors-bundle konfigurieren: allow_origin (Frontend-URL), allow_headers (Authorization, Content-Type), allow_methods (inkl. OPTIONS für Preflight).
3JWT-Token-Speicherung im Frontend?
sessionStorage als Kompromiss (Tab-Schließen löscht Token). httpOnly-Cookie sicherer gegen XSS. Nie localStorage ohne XSS-Prevention.
4TypeScript-Typen aus OpenAPI generieren?
Spec unter /api/docs.json abrufen, openapi-typescript + openapi-fetch generieren typsicheren Client daraus.
5Warum React Query statt useEffect?
Loading/Error/Success-States, Caching, Re-Fetching und optimistic Updates — ohne hunderte Zeilen Boilerplate.
6Validierungsfehler von API Platform in React?
violations-Array (HTTP 422) mit setError(fieldName, { message }) auf React Hook Form Felder mappen.
7API Platform auch mit GraphQL?
Ja. api-platform/graphql installieren aktiviert /api/graphql. Im Frontend Apollo Client oder urql nutzen.
8React + Symfony getrennt deployen?
Symfony als PHP-FPM hinter Nginx. React als statisches Bundle über CDN oder nginx. API-URL aus Umgebungsvariable.
9Paginierung mit API Platform und React Query?
API Platform paginiert mit ?page=1 und hydra:view-Links. useInfiniteQuery liest diese für Infinite-Scroll automatisch aus.
10Empfohlene Symfony-Version?
Symfony 7.x mit PHP 8.4 und API Platform 3.x — vollständige Attribute-Konfiguration, beste Performance, aktiver LTS-Support.