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.
Inhaltsverzeichnis
- 1. Warum React und Symfony zusammen
- 2. API Platform: REST-API in Minuten
- 3. CORS korrekt konfigurieren
- 4. JWT-Authentifizierung mit LexikJWTBundle
- 5. Typsicherer API-Client aus OpenAPI generieren
- 6. React Query für Datenfetching und Caching
- 7. Formulare mit React Hook Form und Validierung
- 8. Fehlerbehandlung und API-Fehlermapping
- 9. Architekturansätze im Vergleich
- 10. Zusammenfassung
- 11. FAQ
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].