SPA-Gefühl ohne eigene API
Symfony mit Inertia.js zu kombinieren bedeutet, Controller weiterhin klassisch Twig-artig zu schreiben, nur dass am Ende statt HTML ein React-Komponentenname mit Props zurückgegeben wird, wodurch das gesamte Frontend wie eine Single-Page-Application wirkt, ohne dass irgendwo eine separate REST- oder GraphQL-API entsteht.
Inhaltsverzeichnis
- 1. Was Symfony mit Inertia.js grundlegend anders macht
- 2. Installation: Backend-Adapter und Frontend-Bridge
- 3. Der erste Inertia-Controller mit Props
- 4. Client-seitiges Routing ohne eigenes Backend-Routing-Duplikat
- 5. Formulare und Validierungsfehler an React weiterreichen
- 6. Geteilte Daten: Auth-Status auf jeder Seite verfügbar machen
- 7. Partial Reloads: nur benötigte Props nachladen
- 8. Server-Side Rendering für SEO-relevante Seiten
- 9. Symfony mit Inertia.js im Vergleich zu API Platform
- 10. Zusammenfassung
- 11. FAQ
1. Was Symfony mit Inertia.js grundlegend anders macht
Wer Symfony mit Inertia.js kombiniert, verzichtet bewusst auf den sonst üblichen Weg, eine separate JSON-API zu bauen, die ein React-Frontend über Fetch-Aufrufe konsumiert. Stattdessen bleibt der Symfony-Controller die einzige Quelle der Wahrheit für eine Seite: Er lädt Daten aus Doctrine, prüft Berechtigungen über Voters, und gibt am Ende statt eines Twig-Templates den Namen einer React-Komponente zusammen mit den benötigten Props zurück. Inertia übernimmt im Hintergrund den Austausch der Seite im Browser, ohne einen kompletten Neuladen der Seite auszulösen.
Der entscheidende konzeptionelle Unterschied bei Symfony mit Inertia.js gegenüber einer klassischen API-plus-SPA-Architektur: Es gibt keine zweite Schnittstelle, die parallel zur Server-Logik gepflegt werden muss. Routing, Autorisierung und Serialisierung passieren an genau einer Stelle im Symfony-Controller, nicht doppelt in Controller und API-Ressource. Das reduziert den Wartungsaufwand erheblich, besonders in Projekten, in denen die API ohnehin nur vom eigenen Frontend genutzt wird und keine externen Konsumenten bedient.
Für Teams, die React-Komponenten für ihre Interaktivität schätzen, aber die Komplexität einer vollständigen API-Schicht mit OpenAPI-Dokumentation und Versionierung vermeiden wollen, ist Symfony mit Inertia.js ein pragmatischer Mittelweg zwischen klassischem, serverseitig gerendertem Symfony und einer vollständig entkoppelten Single-Page-Application.
2. Installation: Backend-Adapter und Frontend-Bridge
Die Einrichtung von Symfony mit Inertia.js benötigt zwei Teile: den PHP-seitigen Adapter inertiajs/inertia-laravel existiert nur für Laravel, für Symfony wird stattdessen die Community-Bibliothek inertiajs/inertia-symfony beziehungsweise ein leichtgewichtiger eigener Service genutzt, der die HTTP-Vertragsdetails von Inertia implementiert. Auf der Frontend-Seite kommt das npm-Paket @inertiajs/react zum Einsatz, das den React-Root aufsetzt und bei Navigationen die passende Seite lädt.
Wichtig bei der Konfiguration von Symfony mit Inertia.js: Das Backend muss auf den speziellen HTTP-Header X-Inertia reagieren. Ist der Header gesetzt, antwortet der Server mit reinem JSON statt einem vollständigen HTML-Dokument, weil Inertia im Frontend bereits eine laufende Seite hat und nur die neuen Props benötigt. Bei einem regulären Seitenaufruf ohne diesen Header liefert Symfony ein initiales HTML-Dokument mit eingebettetem JSON, aus dem React beim ersten Laden hydriert.
# Backend: install the Inertia HTTP contract adapter for Symfony
composer require inertiajs/inertia-symfony
# Frontend: install the Inertia React client
bin/console importmap:require @inertiajs/react
bin/console importmap:require react react-dom
3. Der erste Inertia-Controller mit Props
Ein Symfony-Controller, der Inertia.js nutzt, unterscheidet sich strukturell kaum von einem gewöhnlichen Controller. Statt $this->render('template.html.twig', [...]) aufzurufen, wird eine Inertia-Response mit dem Namen der React-Komponente und einem Array von Props zurückgegeben. Die Komponente selbst liegt als .tsx-Datei im Frontend-Verzeichnis und erhält die Props als reguläre React-Props, typsicher über TypeScript-Interfaces.
Der Vorteil dieses Musters bei Symfony mit Inertia.js: Die Struktur der Props wird zentral im Controller definiert, dort, wo auch die Autorisierungsprüfung über denyAccessUnlessGranted stattfindet. Es gibt keine separate Serializer-Konfiguration und keine zusätzliche API-Ressourcen-Klasse, die synchron zum Controller gehalten werden müsste.
// src/Controller/OrderController.php
namespace App\Controller;
use App\Repository\OrderRepository;
use Inertia\Inertia;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Attribute\Route;
final class OrderController extends AbstractController
{
public function __construct(
private readonly OrderRepository $orderRepository,
) {
}
#[Route('/orders', name: 'app_orders_index', methods: ['GET'])]
public function index(): Response
{
$this->denyAccessUnlessGranted('ROLE_USER');
$orders = $this->orderRepository->findRecentForCurrentUser(limit: 25);
// Renders the React component "Orders/Index" with these props
return Inertia::render('Orders/Index', [
'orders' => array_map(
static fn ($order) => [
'id' => $order->getId(),
'reference' => $order->getReference(),
'total' => $order->getTotalGross(),
'status' => $order->getStatus()->value,
],
$orders,
),
]);
}
}
4. Client-seitiges Routing ohne eigenes Backend-Routing-Duplikat
Ein häufiges Missverständnis bei Symfony mit Inertia.js: Es entsteht kein zweites Routing-System im Frontend, das parallel zu Symfonys routes.yaml gepflegt werden müsste. Navigation zwischen Seiten läuft über die Inertia-Link-Komponente, die intern eine reguläre Symfony-URL anspricht, etwa /orders/42. Der Browser wechselt die URL sichtbar in der Adressleiste, Inertia fängt den Klick jedoch ab, verhindert den vollständigen Seiten-Reload und lädt nur die neuen Props per Fetch nach.
Das bedeutet für Symfony mit Inertia.js: Die einzige Quelle der Wahrheit für URLs bleibt Symfonys Routing-Komponente. Um im React-Code typsichere URL-Generierung zu ermöglichen, nutzen viele Projekte zusätzlich ein kleines Code-Generierungs-Tool, das aus den Symfony-Routen eine TypeScript-Datei mit Hilfsfunktionen erzeugt, sodass Routen-Änderungen im Backend sofort als Compile-Fehler im Frontend sichtbar werden, statt erst zur Laufzeit als kaputter Link aufzufallen.
5. Formulare und Validierungsfehler an React weiterreichen
Formulare sind der Bereich, in dem Symfony mit Inertia.js seine größte Stärke gegenüber einer reinen API-Architektur zeigt. Der Inertia-Frontend-Client stellt einen useForm-Hook bereit, der Formulardaten hält, den Submit per POST an die Symfony-Route sendet und bei einer Validierungsfehler-Response automatisch die Fehler in ein errors-Objekt einsortiert, ohne dass eigener Fehlerbehandlungscode im Frontend geschrieben werden muss.
Auf Symfony-Seite genügt es, bei einer fehlgeschlagenen Validierung mit dem regulären Symfony-Validator eine Inertia-spezifische Fehler-Response zurückzugeben, die die Feld-zu-Fehlermeldung-Zuordnung als assoziatives Array enthält. Inertia erkennt diese Antwort automatisch am HTTP-Statuscode 422 und aktualisiert die Formular-Fehler im React-Zustand, ohne die aktuelle Seite zu verlassen. Für Symfony mit Inertia.js bedeutet das: Dieselbe Validierungslogik, die auch bei klassischen Twig-Formularen genutzt wird, funktioniert unverändert weiter.
// resources/js/Pages/Orders/Create.tsx
import { useForm } from '@inertiajs/react';
export default function Create() {
// Inertia's useForm tracks data, errors and submission state together
const { data, setData, post, processing, errors } = useForm({
customerName: '',
total: 0,
});
function submit(event: React.FormEvent) {
event.preventDefault();
post('/orders'); // Sends a POST to the Symfony route, errors flow into `errors`
}
return (
<form onSubmit={submit}>
<input
value={data.customerName}
onChange={(e) => setData('customerName', e.target.value)}
/>
{errors.customerName && <span>{errors.customerName}</span>}
<button type="submit" disabled={processing}>Speichern</button>
</form>
);
}
6. Geteilte Daten: Auth-Status auf jeder Seite verfügbar machen
Nicht jede Information, die eine React-Seite benötigt, sollte aus jedem einzelnen Controller heraus wiederholt werden, etwa der Name des eingeloggten Benutzers oder eine ungelesene Benachrichtigungs-Zahl. Symfony mit Inertia.js löst das über geteilte Props, die zentral an einer Stelle definiert werden und automatisch jeder Inertia-Response beigemischt werden, unabhängig davon, welcher Controller die Anfrage bearbeitet.
Diese geteilten Daten werden über einen zentralen Middleware-artigen Mechanismus bereitgestellt, der vor jeder Inertia-Response ausgeführt wird. Für Symfony mit Inertia.js bietet sich dafür ein Event-Listener auf kernel.controller oder ein dedizierter Service an, der den aktuellen Security-Token ausliest und die relevanten Benutzerdaten in die geteilten Props einspeist, bevor die eigentliche Controller-Logik läuft.
// src/EventListener/InertiaSharedDataListener.php
namespace App\EventListener;
use Inertia\Inertia;
use Symfony\Bundle\SecurityBundle\Security;
use Symfony\Component\EventDispatcher\Attribute\AsEventListener;
use Symfony\Component\HttpKernel\Event\ControllerEvent;
use Symfony\Component\HttpKernel\KernelEvents;
#[AsEventListener(event: KernelEvents::CONTROLLER)]
final class InertiaSharedDataListener
{
public function __construct(
private readonly Security $security,
) {
}
public function __invoke(ControllerEvent $event): void
{
$user = $this->security->getUser();
// Shared props are merged into every Inertia response automatically
Inertia::share('auth', [
'user' => $user ? ['name' => $user->getUserIdentifier()] : null,
]);
}
}
7. Partial Reloads: nur benötigte Props nachladen
Eine Seite mit mehreren unabhängigen Datenblöcken, etwa einer Bestellliste und einem separaten Statistik-Widget, muss bei einer Filteränderung nicht zwingend alle Props neu berechnen. Symfony mit Inertia.js unterstützt Partial Reloads, bei denen das Frontend über den only-Parameter gezielt anfordert, welche Props aktualisiert werden sollen. Auf Symfony-Seite lässt sich das über Inertia::lazy() abbilden, wodurch ein teurer Prop-Wert nur berechnet wird, wenn er tatsächlich angefragt wurde.
Dieser Mechanismus ist besonders wertvoll bei rechenintensiven Aggregationen, etwa einer Umsatzstatistik, die über mehrere Tabellen joint. Ohne Partial Reloads würde Symfony mit Inertia.js diese Berechnung bei jeder kleinen Filteränderung erneut ausführen, selbst wenn nur die Bestellliste betroffen ist. Mit Inertia::lazy() wird die teure Berechnung übersprungen, solange das Frontend das entsprechende Prop nicht explizit anfordert.
8. Server-Side Rendering für SEO-relevante Seiten
Standardmäßig rendert Symfony mit Inertia.js die React-Komponenten ausschließlich im Browser, was für Backend-Anwendungen wie Bestellverwaltung oder Kundenportale meist ausreicht, für öffentlich zugängliche, SEO-relevante Seiten aber problematisch ist, weil Suchmaschinen-Crawler ohne ausgeführtes JavaScript nur eine leere Seite sehen. Für diese Fälle unterstützt Inertia optionales Server-Side Rendering über einen separaten Node.js-Prozess, der dieselben React-Komponenten serverseitig zu HTML rendert, bevor Symfony die Antwort ausliefert.
Der Betrieb dieses SSR-Prozesses ist der einzige Punkt, an dem Symfony mit Inertia.js doch wieder eine Node.js-Laufzeit im Produktionsbetrieb benötigt, zusätzlich zu PHP-FPM. In der Praxis wird dieser Node-Prozess als eigener systemd-Service oder Container neben dem PHP-Prozess betrieben, und Symfony leitet Anfragen für SSR-relevante Seiten intern an ihn weiter, bevor das finale HTML an den Browser geht.
9. Symfony mit Inertia.js im Vergleich zu API Platform
Die Entscheidung zwischen Symfony mit Inertia.js und einer vollständigen API-Architektur mit API Platform hängt maßgeblich davon ab, ob das Frontend jemals von einem anderen Client als der eigenen Webanwendung konsumiert werden soll. Die folgende Übersicht stellt die wichtigsten Unterschiede gegenüber.
| Aspekt | Klassische REST-API + SPA | Symfony mit Inertia.js |
|---|---|---|
| Separate API-Schicht nötig | Ja | Nein |
| Client-Routing dupliziert Backend-Routing | Meist ja | Nein, Symfony bleibt Quelle |
| Validierungsfehler-Handling | Selbst gebaut | Eingebaut über useForm |
| Externe API-Konsumenten möglich | Ja | Nein, intern gekoppelt |
| SEO ohne zusätzlichen Aufwand | Abhängig vom Setup | Nur mit optionalem SSR-Prozess |
Wer eine API benötigt, die auch von einer mobilen App oder externen Partnern konsumiert wird, kommt an einer echten REST- oder GraphQL-Schicht wie API Platform nicht vorbei. Symfony mit Inertia.js ist die richtige Wahl, wenn das Frontend ausschließlich für die eigene Webanwendung existiert und keine zweite Schnittstelle gepflegt werden soll.
Mironsoft
Symfony-Entwicklung mit modernem React-Frontend
SPA-Gefühl ohne eine zweite API pflegen zu müssen?
Wir bauen Symfony-Anwendungen mit Inertia.js und React, inklusive Formular-Validierung, geteilten Daten und optionalem Server-Side Rendering für SEO-relevante Seiten.
Architektur-Beratung
Inertia.js vs. API Platform für euren Anwendungsfall bewerten
Implementierung
Controller, React-Seiten und geteilte Props sauber aufsetzen
SSR-Betrieb
Server-Side Rendering für öffentliche, SEO-relevante Seiten einrichten
10. Zusammenfassung
Symfony mit Inertia.js zu kombinieren bedeutet, Controller weiterhin zentral als einzige Quelle der Wahrheit für Routing, Autorisierung und Datenzugriff zu behalten, während React für die Interaktivität im Browser sorgt. Statt einer separaten JSON-API liefern Controller direkt React-Komponentennamen mit Props, Formulare nutzen den eingebauten useForm-Hook für Validierungsfehler, und geteilte Props sorgen dafür, dass Auth-Status und andere globale Daten nicht in jedem Controller wiederholt werden müssen.
Der größte Kompromiss bei Symfony mit Inertia.js ist der Verzicht auf eine eigenständige, extern konsumierbare API. Wer das nicht braucht, spart erheblichen Wartungsaufwand gegenüber einer klassischen REST-API-plus-SPA-Architektur. Für öffentliche, SEO-relevante Seiten lohnt sich der zusätzliche Aufwand für Server-Side Rendering über einen begleitenden Node.js-Prozess, während interne Anwendungen ohne SSR völlig ausreichend funktionieren.
Symfony mit Inertia.js — Das Wichtigste auf einen Blick
Kein zweites API
Controller liefern Komponentennamen und Props direkt, ohne separate JSON-Ressourcen-Klassen.
Formulare eingebaut
useForm-Hook verarbeitet Validierungsfehler automatisch über den HTTP-Statuscode 422.
Geteilte Props
Auth-Status und globale Daten zentral über einen Event-Listener bereitstellen.
SSR optional
Node.js-Prozess nur für öffentliche, SEO-relevante Seiten nötig, sonst reines Client-Rendering.