SF
{ }
Symfony · React · API Platform · Fullstack · TypeScript
Symfony + React:
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.

20 Min. Lesezeit JWT · TypeScript-Codegen · React Query · CORS · Deployment Symfony 7.x · API Platform 4.x · React 19 · PHP 8.4

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.

11. FAQ: Symfony + React Fullstack mit API Platform

1Monorepo für Symfony + React notwendig?
Nicht zwingend. Monorepo vereinfacht atomare Commits und CI. Getrennte Repos sinnvoll bei mehreren Clients oder verschiedenen Teams.
2TypeScript-Typen aus API Platform generieren?
@hey-api/openapi-ts liest /api/docs.json und generiert typsichere Clients. In CI-Pipeline nach Backend-Deploy integrieren.
3CORS-Fehler mit React und Symfony beheben?
nelmio/cors-bundle konfigurieren. allow_credentials: true erfordert explizite Domain. Authorization in allow_headers aufnehmen.
4JWT mit Symfony und React implementieren?
lexik/jwt-authentication-bundle in Symfony. React sendet Token als Authorization: Bearer. Zentraler API-Wrapper hängt Token automatisch an. 401 → Token-Refresh oder Login-Redirect.
5React Query für Symfony-APIs sinnvoll?
Ja. Caching, Hintergrund-Refetch und automatische Invalidierung nach Mutations halten die UI konsistent — ersetzt manuelles State-Management vollständig.
6Symfony-Validierungsfehler im React-Formular?
HTTP 422 mit violations-Array parsen. propertyPath auf React Hook Form Felder via setError() mappen. Kein manuelles Fehler-Mapping pro Komponente nötig.
7Mehrere Frontend-Clients an einer Symfony-API?
Ja. Dieselbe API Platform-API bedient Web, Mobile und Drittanbieter. Jeder Client generiert seinen TypeScript-Client aus demselben OpenAPI-Schema.
8Deployment von Symfony + React Fullstack?
Backend: nginx + php-fpm. Frontend: statischer Build über CDN oder nginx. API-URL als Umgebungsvariable im Frontend-Build-Prozess setzen.
9REST oder GraphQL für React?
REST mit OpenAPI-Codegen reicht für die meisten Apps. GraphQL lohnt sich bei stark variierenden Datenformen und messbarem Over-Fetching-Problem.
10Wie teste ich Symfony-API + React gemeinsam?
PHPUnit für Symfony-Endpunkte. Cypress/Playwright für E2E. React Testing Library + msw für Frontend-Unit-Tests ohne echte API-Verbindung.