Fullstack-App mit API Platform
Symfony als Backend-Framework, API Platform als REST-Schicht und React als Frontend bilden einen Stack, bei dem jede Schicht ihre Stärken ausspielt. Das Ergebnis: eine typsichere Fullstack-Applikation, bei der der TypeScript-Client aus dem OpenAPI-Schema generiert wird und JWT-Authentifizierung sauber in beide Schichten integriert ist.
Inhaltsverzeichnis
- 1. Warum Symfony + React + API Platform so gut zusammenpassen
- 2. Projektstruktur: Monorepo vs. getrennte Repositories
- 3. CORS korrekt konfigurieren
- 4. JWT-Authentifizierung in Symfony und React
- 5. TypeScript-Client aus OpenAPI generieren
- 6. React Query für API-Zustand und Caching
- 7. Formulare mit React Hook Form und Symfony-Validation
- 8. Fehlerbehandlung: API-Fehler typsicher im Frontend
- 9. Stack-Vergleich: Symfony+React vs. Alternativen
- 10. Zusammenfassung
- 11. FAQ
1. Warum Symfony + React + API Platform so gut zusammenpassen
Die Kombination aus Symfony, API Platform und React ist kein Zufall, sondern das Ergebnis einer klaren Aufgabenteilung. Symfony übernimmt alles, was auf dem Server passiert: Geschäftslogik, Datenbankzugriffe, Authentifizierung und Autorisierung. API Platform verwandelt die Symfony-Domänenklassen deklarativ in REST-Endpunkte mit automatischer OpenAPI-Dokumentation. React rendert die Benutzeroberfläche, verwaltet lokalen State und kommuniziert ausschließlich über HTTP mit der Symfony-API. Jede Schicht kann unabhängig entwickelt, getestet und skaliert werden.
Der entscheidende Vorteil gegenüber einem monolithischen Symfony-Frontend mit Twig-Templates liegt in der klaren Schnittstellendefinition. Das OpenAPI-Schema, das API Platform automatisch generiert, ist die einzige Vertragsdefinition zwischen Backend und Frontend. Ein TypeScript-Codegen-Tool liest dieses Schema und generiert typsichere Client-Funktionen — Tippfehler in API-Pfaden oder falsche Parametertypen fallen sofort beim Build auf, nicht erst im Produktionsbetrieb. Symfony als Backend bleibt dabei vollständig unberührt von Frontend-Entscheidungen: React könnte jederzeit durch Vue.js oder eine mobile App ersetzt werden, ohne eine Zeile Symfony-Code zu ändern.
2. Projektstruktur: Monorepo vs. getrennte Repositories
Bei einer Symfony-React-Fullstack-App stellt sich sofort die Frage nach der Repository-Struktur. Das Monorepo-Modell legt Backend und Frontend in dasselbe Repository: /backend für das Symfony-Projekt, /frontend für die React-App. Der Vorteil ist atomare Commits, die Backend und Frontend gemeinsam versionieren, und eine einzige CI/CD-Pipeline für den gesamten Stack. Der Nachteil ist, dass Teams, die Backend und Frontend strikt trennen, gegenseitig in Code-Reviews eingebunden werden müssen.
Getrennte Repositories machen Sinn, wenn Backend und Frontend von verschiedenen Teams verantwortet werden oder wenn das Symfony-Backend mehrere Frontend-Clients bedient – eine Web-App und eine mobile App gleichzeitig. In diesem Fall koordiniert das OpenAPI-Schema die Schnittstelle: Das Backend-Team veröffentlicht Schema-Änderungen, das Frontend-Team regeneriert den TypeScript-Client und passt die Implementierung an. Für die meisten mittelgroßen Projekte ist das Monorepo die praktischere Wahl, weil es den Koordinationsaufwand reduziert und lokale Entwicklung mit einem einzigen docker compose up ermöglicht.
<?php
// Monorepo structure for Symfony + React fullstack app
// backend/ → Symfony 7 application with API Platform
// frontend/ → React 19 application with TypeScript
// backend/composer.json (relevant packages)
// "require": {
// "api-platform/core": "^4.0",
// "api-platform/doctrine-orm": "^4.0",
// "lexik/jwt-authentication-bundle": "^3.0",
// "nelmio/cors-bundle": "^2.5",
// "symfony/security-bundle": "^7.0"
// }
// frontend/package.json (relevant packages)
// "dependencies": {
// "react": "^19.0",
// "@tanstack/react-query": "^5.0",
// "react-hook-form": "^7.0",
// "zod": "^3.0"
// },
// "devDependencies": {
// "@hey-api/openapi-ts": "^0.50.0",
// "typescript": "^5.4"
// }
// docker-compose.yml brings both services up:
// backend: symfony local server or php-fpm + nginx on port 8000
// frontend: vite dev server on port 5173 with proxy to backend
3. CORS korrekt konfigurieren
Der häufigste Fehler beim ersten Start einer Symfony-React-App ist ein CORS-Fehler im Browser. React läuft auf Port 5173 (Vite) oder 3000 (Create React App), Symfony auf Port 8000. Der Browser blockiert Cross-Origin-Requests, wenn der Server keine entsprechenden CORS-Header zurückschickt. Das nelmio/cors-bundle ist die Standardlösung in Symfony für CORS-Konfiguration und integriert sich sauber in den Symfony-Request-Lifecycle.
Die Konfiguration in config/packages/nelmio_cors.yaml legt fest, welche Origins erlaubt sind, welche HTTP-Methoden akzeptiert werden und ob Credentials (Cookies, Authorization-Header) mitgeschickt werden dürfen. Für die Entwicklungsumgebung erlaubt man den lokalen Vite-Dev-Server explizit. In der Produktionsumgebung trägt man die finale Domain ein. Wichtig: allow_credentials: true erfordert, dass allow_origin keine Wildcard (*) ist, sondern eine explizite Domain. Der Authorization-Header muss in allow_headers gelistet sein, damit JWT-Tokens in Symfony-Requests durchkommen.
4. JWT-Authentifizierung in Symfony und React
JWT-Authentifizierung ist der Standard für Symfony-APIs mit React-Frontends, weil JWT-Tokens zustandslos sind und keine Session-Verwaltung auf dem Server erfordern. Das lexik/jwt-authentication-bundle übernimmt die Token-Generierung und -Validierung in Symfony. Nach erfolgreicher Anmeldung über den /auth/token-Endpunkt liefert Symfony ein JWT zurück, das der React-Client im localStorage oder einem HttpOnly-Cookie speichert. Jeder folgende API-Request enthält den Token im Authorization: Bearer-Header.
Auf der React-Seite implementiert man einen API-Client-Wrapper, der den Token automatisch an jeden Request anhängt. Mit React Query definiert man einen globalen queryClient, dessen defaultOptions einen gemeinsamen Fetch-Wrapper nutzen. Wenn Symfony einen 401-Status zurückgibt, löst React Query automatisch einen Redirect zur Login-Seite aus oder erneuert den Token über einen Refresh-Endpunkt. Token-Refresh-Logik gehört in einen zentralen Interceptor und nicht in jeden einzelnen React-Komponenten, um den Code wartbar zu halten.
<?php
declare(strict_types=1);
namespace App\Controller;
use App\Entity\User;
use Lexik\Bundle\JWTAuthenticationBundle\Services\JWTTokenManagerInterface;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\JsonResponse;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\PasswordHasher\Hasher\UserPasswordHasherInterface;
use Symfony\Component\Routing\Attribute\Route;
use Symfony\Component\Security\Core\Exception\BadCredentialsException;
use Doctrine\ORM\EntityManagerInterface;
/**
* Handles JWT token issuance for React frontend authentication.
*/
#[Route('/auth')]
final class AuthController extends AbstractController
{
public function __construct(
private readonly JWTTokenManagerInterface $jwtManager,
private readonly UserPasswordHasherInterface $passwordHasher,
private readonly EntityManagerInterface $entityManager,
) {}
/**
* Issue a JWT token for valid credentials.
* POST /auth/token { "email": "...", "password": "..." }
*/
#[Route('/token', methods: ['POST'])]
public function token(Request $request): JsonResponse
{
$data = json_decode($request->getContent(), true);
$email = $data['email'] ?? '';
$password = $data['password'] ?? '';
$user = $this->entityManager
->getRepository(User::class)
->findOneBy(['email' => $email]);
if (!$user || !$this->passwordHasher->isPasswordValid($user, $password)) {
throw new BadCredentialsException('Invalid credentials.');
}
// Token contains user roles, expiry and custom claims
return $this->json([
'token' => $this->jwtManager->create($user),
'expires_in' => 3600,
'user' => ['email' => $user->getEmail(), 'roles' => $user->getRoles()],
]);
}
}
5. TypeScript-Client aus OpenAPI generieren
Einer der größten Vorteile der Symfony-React-Kombination mit API Platform ist die automatische Codegenerierung. Das Tool @hey-api/openapi-ts liest das OpenAPI-JSON, das API Platform unter /api/docs.json bereitstellt, und generiert typsichere TypeScript-Interfaces und Client-Funktionen für alle Endpunkte. Das bedeutet: Wenn in Symfony ein neues Feld zur Entity hinzugefügt wird, erscheint es nach dem nächsten Codegen-Lauf automatisch im TypeScript-Typ — keine manuelle Synchronisation nötig.
Das Codegen-Script wird als npm-Skript in package.json eingetragen und läuft in der CI-Pipeline nach jedem Backend-Deployment. Der generierte Code landet in einem separaten Ordner (src/api/generated), der nicht manuell bearbeitet wird. Eigene Wrapper-Hooks in src/api/hooks verwenden die generierten Typen und Funktionen, fügen aber React-Query-Integration, Caching-Konfiguration und Fehlerbehandlung hinzu. So bleibt der generierte Code sauber und die eigene Logik ist klar abgegrenzt. Symfony-Validierungsfehler im RFC-7807-Format (API Platform Standard) werden vom Frontend-Wrapper typsicher geparst und als Formularfehler weitergereicht.
6. React Query für API-Zustand und Caching
React Query (TanStack Query) ist das Standard-Tool für Server-State-Management in React-Apps, die mit einer Symfony-API kommunizieren. Das Grundprinzip: Jeder API-Aufruf wird als Query mit einem einzigartigen Key definiert. React Query cacht das Ergebnis, zeigt stale Daten sofort an, refetcht im Hintergrund und synchronisiert automatisch, wenn der Browser-Tab wieder aktiv wird. Für eine Symfony-Backend-App bedeutet das: Die Produktliste wird sofort aus dem Cache gerendert, während im Hintergrund ein frischer Request an die Symfony-API geht.
Mutations in React Query kapseln POST, PUT, PATCH und DELETE-Requests an Symfony. Nach einer erfolgreichen Mutation invalidiert man die betroffenen Queries, damit React Query die Daten neu abruft. Das klassische Muster: Nach dem Erstellen eines Produkts via POST /api/products wird der Query-Key ['products'] invalidiert, React Query refetcht die Liste automatisch und die UI zeigt das neue Produkt ohne manuellen State-Update. Optimistic Updates zeigen das Ergebnis sofort in der UI an und rollen bei einem Symfony-Fehler automatisch zurück.
// React Query + generated Symfony API client — frontend/src/api/hooks/useProducts.ts
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
// Import from generated client (from Symfony OpenAPI schema)
import { ProductsService, type Product } from '../generated';
// Query key factory — centralised to avoid typos
export const productKeys = {
all: () => ['products'] as const,
detail: (id: number) => ['products', id] as const,
};
// Fetch product list — React Query caches and revalidates automatically
export function useProducts() {
return useQuery({
queryKey: productKeys.all(),
queryFn: () => ProductsService.getProductCollection(),
staleTime: 60_000, // 60 seconds before background refetch
});
}
// Create product — invalidates list after success
export function useCreateProduct() {
const queryClient = useQueryClient();
return useMutation({
mutationFn: (data: Omit<Product, 'id'>) =>
ProductsService.postProductCollection({ requestBody: data }),
onSuccess: () => {
// Symfony returned 201 — invalidate the list so it refetches
queryClient.invalidateQueries({ queryKey: productKeys.all() });
},
onError: (error) => {
// Symfony API Platform returns RFC-7807 violation format on 422
console.error('Symfony validation error:', error);
},
});
}
7. Formulare mit React Hook Form und Symfony-Validation
Formularvalidierung in einer Symfony-React-App findet auf zwei Ebenen statt: clientseitig mit React Hook Form und Zod, serverseitig mit Symfony's Validator-Komponente. Beide Ebenen prüfen dieselben Regeln — NotBlank, Mindestlängen, E-Mail-Format — aber aus unterschiedlichen Gründen. Clientseitige Validierung gibt sofortiges Feedback ohne Netzwerkanfrage. Serverseitige Validierung in Symfony ist die echte Sicherheitslinie, weil clientseitiger Code immer umgehbar ist.
Wenn Symfony eine Validierungsverletzung als HTTP 422 zurückgibt, enthält die Antwort im API-Platform-Format ein violations-Array mit propertyPath und message. Der React-Hook-Form-Wrapper parst dieses Array und setzt die Fehler mit form.setError(propertyPath, { message }) direkt auf die betroffenen Felder. Das Formular zeigt dann unter jedem Eingabefeld den Symfony-Validierungsfehler in der richtigen Sprache an — ohne manuelles Fehler-Mapping im Frontend-Code. Zod-Schemas auf dem Client spiegeln die Symfony-Constraints und fangen die offensichtlichsten Fehler ab, bevor der Request überhaupt gesendet wird.
8. Fehlerbehandlung: API-Fehler typsicher im Frontend
API Platform auf Symfony gibt Fehler im RFC-7807-Format zurück: ein JSON-Objekt mit type, title, status und detail. Validierungsfehler (422) enthalten zusätzlich ein violations-Array. Authentifizierungsfehler (401) haben einen Standard-Body. Berechtigungsfehler (403) und Not-Found-Fehler (404) folgen demselben Format. Der Frontend-Code muss diese Antworten typsicher verarbeiten, statt generische any-Typen zu nutzen.
Ein zentraler API-Error-Handler definiert TypeScript-Interfaces für alle Symfony-Fehlerformate und eine Type-Guard-Funktion, die prüft, ob eine Fetch-Response dem RFC-7807-Format entspricht. React Query's onError-Callback empfängt den geparsten Fehler und entscheidet: Validierungsfehler werden ans Formular weitergereicht, 401-Fehler lösen einen Token-Refresh aus, 403-Fehler zeigen eine Zugriff-verweigert-Meldung. Dieser zentrale Fehler-Handler vermeidet, dass jede React-Komponente eigene Symfony-Fehler-Parsing-Logik implementiert.
9. Stack-Vergleich: Symfony+React vs. Alternativen
Die Wahl des Fullstack-Stacks hängt von Teamkompetenz, Skalierungsanforderungen und Wartbarkeit ab. Symfony mit React ist nicht der einzige sinnvolle Ansatz, aber einer mit klaren Stärken bei mittelgroßen bis großen Projekten mit komplexer Geschäftslogik.
| Kriterium | Symfony + React | Next.js (Fullstack) | Laravel + Inertia |
|---|---|---|---|
| Typsicherheit | OpenAPI → TypeScript-Codegen | TypeScript end-to-end | PHP + TypeScript getrennt |
| Skalierbarkeit | Backend und Frontend getrennt skalierbar | Server-Komponenten + Edge | Monolith, schwerer zu trennen |
| API-Dokumentation | Automatisch via API Platform | Manuell oder tRPC | Manuell oder Scribe |
| Komplexe Geschäftslogik | Symfony DI, Events, Messenger | Node.js-Grenzen | Laravel gut, aber PHP weniger strikt |
| Einstiegshürde | Höher (zwei Stacks lernen) | Niedriger (ein Stack) | Mittel |
Die Tabelle zeigt: Symfony mit React ist der richtige Stack, wenn Geschäftslogik komplex ist, die API mehrere Clients bedient und Typsicherheit durch Codegen eine Priorität ist. Next.js als Fullstack-Framework ist schneller gestartet, aber schlechter geeignet für Projekte mit umfangreicher Serverlogik, die PHP-Bibliotheken oder bestehenden PHP-Code nutzt. Laravel mit Inertia ist eine gute Wahl für Teams, die keinen API-Layer wollen und eine enge Kopplung zwischen Backend und Frontend akzeptieren.
Mironsoft
Symfony API-Entwicklung, React-Integration und Fullstack-Architektur
Symfony + React Fullstack-App entwickeln?
Wir bauen typsichere Fullstack-Applikationen mit Symfony, API Platform und React — von der JWT-Authentifizierung über TypeScript-Codegen bis zum produktionsreifen Deployment für euren Stack.
API-Architektur
Symfony Backend mit API Platform, JWT und CORS für React-Frontends konfigurieren
TypeScript-Codegen
OpenAPI-zu-TypeScript-Pipeline aufbauen und in CI integrieren
React Integration
React Query, Formularvalidierung und Fehlerbehandlung mit Symfony-API verbinden
10. Zusammenfassung
Die Symfony-React-Fullstack-App mit API Platform ist ein Stack, der die Stärken beider Welten vereint: Symfony liefert robuste Geschäftslogik, strikte Typisierung in PHP 8.4 und ein ausgereiftes Ökosystem für Authentifizierung, Messaging und Datenbankzugriffe. API Platform generiert REST-Endpunkte, OpenAPI-Dokumentation und optionales GraphQL deklarativ aus PHP-Klassen. React übernimmt das Frontend mit modernem State-Management durch React Query und typsichere Formularvalidierung. Der TypeScript-Codegen-Schritt schließt den Kreis: Änderungen am Symfony-Backend propagieren automatisch als Typen ins React-Frontend.
Der wichtigste Hebel ist die Automatisierung der Codegen-Pipeline. Solange Backend und Frontend über das OpenAPI-Schema synchronisiert bleiben, entstehen keine Schnittstellenfehler in der Produktion. JWT-Authentifizierung, CORS-Konfiguration und zentrales Fehler-Handling sind Infrastrukturaufgaben, die einmal sauber implementiert werden müssen — danach konzentriert sich die Entwicklung vollständig auf Domänenlogik in Symfony und Benutzeroberfläche in React.
Symfony + React Fullstack — Das Wichtigste auf einen Blick
OpenAPI → TypeScript
API Platform generiert OpenAPI-Schema automatisch. @hey-api/openapi-ts macht daraus typsichere TypeScript-Clients — keine manuelle Synchronisation.
JWT + CORS
lexik/jwt-authentication-bundle für Token-Ausgabe, nelmio/cors-bundle für CORS-Header. Authorization-Header in allow_headers aufnehmen.
React Query
Server-State mit React Query verwalten: Queries für GET, Mutations für POST/PUT/DELETE. Invalidierung nach Mutations hält die UI konsistent.
Fehlerbehandlung
RFC-7807-Fehler von Symfony typsicher im Frontend parsen. Validierungsverletzungen als Formularfehler via React Hook Form setzen.