Full-Stack-Entwicklung

Laravel API Resources + FormRequests und Angular-TypeScript-Interfaces: sauberer API-Vertrag

Wie Sie mit Laravel API Resources, FormRequests und passenden TypeScript-Interfaces einen sauberen, typsicheren API-Vertrag zwischen Laravel und Angular bauen.

9 Min. LesezeitAutor: Martin TomczakAktualisiert: 28.06.2026
Abstrakte Darstellung eines API-Vertrags: strukturierte Datensaetze fliessen durch eine Filterschicht und richten sich rechts als typisierte Datenbloecke aus.

Warum `return $user;` im Controller eine schlechte Idee ist

Ich habe genug Codebasen gesehen, in denen ein Controller einfach return User::find($id); macht. Funktioniert. Erstmal. Bis jemand dem Modell ein Feld password_reset_token oder internal_notes hinzufügt und dieses Feld plötzlich in der API-Antwort landet. Oder bis die Spalte is_admin umbenannt wird und das Angular-Frontend stumm bricht, weil niemand wusste, dass es diese Kopplung gab.

Das Grundproblem: Wer das Eloquent-Modell direkt serialisiert, macht die interne Datenbankstruktur zum öffentlichen API-Vertrag. Jede Spalte, jede Umbenennung, jede neue Relation wird ungewollt nach außen sichtbar. Genau hier setzt das Zusammenspiel aus Laravel-Backend und Angular-Frontend an, das ich in diesem Cluster beschreibe. Die Architektur ist bewusst entkoppelt: Angular als Single-Page-Application, Laravel als zustandslose API, die den Datenzugriff kontrolliert. Die strikte Trennung ist kein Selbstzweck, sondern die Voraussetzung dafür, dass beide Seiten unabhängig wachsen können. Den Gesamtüberblick liefert der Angular + Laravel Leitfaden, die architektonische Begründung der Entkopplung steht im Beitrag zur Angular-Laravel-Architektur.

Die API Resource: Sie entscheiden, was rausgeht

Eine API Resource ist eine schmale Klasse, die ein Eloquent-Modell entgegennimmt und exakt die JSON-Struktur zurückgibt, die Sie nach außen geben wollen. Kein Feld mehr, kein Feld weniger. Damit kontrollieren Sie die Transformation an einer einzigen Stelle, können Felder umbenennen, formatieren oder ganz weglassen, und Sie behalten die Kontrolle über Versionierung.

So sieht eine UserResource aus:


<?php

namespace App\Http\Resources;

use Illuminate\Http\Request;

use Illuminate\Http\Resources\Json\JsonResource;

class UserResource extends JsonResource

{

public function toArray(Request $request): array

{

return [

'id'        => $this->id,

'name'      => $this->name,

'email'     => $this->email,

'role'      => $this->role,

'createdAt' => $this->created_at->toIso8601String(),

];

}

}

Was hier passiert, ist unscheinbar, aber entscheidend. Die Spalte created_at aus der Datenbank wird zu createdAt im JSON und gleich in ISO-8601 formatiert. Sensible Felder wie password oder interne Flags tauchen schlicht nicht auf, weil sie nicht im Array stehen. Wenn morgen eine neue Spalte dazukommt, leakt sie nicht automatisch. Sie entscheiden bewusst, ob sie in den Vertrag aufgenommen wird.

Im Controller verwenden Sie die Resource so:


public function show(User $user): UserResource

{

return new UserResource($user);

}

Für eine Liste nehmen Sie UserResource::collection($users). Das war's. Mein Rat aus der Projektpraxis: Legen Sie die Resource an, sobald ein Modell überhaupt nach außen geht, nicht erst, wenn das erste Feld geleakt ist. Nachträglich Resources einzuziehen ist deutlich mühsamer, als von Anfang an sauber zu starten.

FormRequest: Validierung gehört nicht in den Controller

Validierung direkt im Controller bläht ihn auf und macht die Regeln schwer wiederverwendbar. Laravel bietet dafür FormRequest-Klassen. Eine FormRequest kapselt sowohl die Autorisierung als auch die Validierungsregeln und wird per Type-Hint in die Controller-Methode injiziert. Schlägt die Validierung fehl, liefert Laravel automatisch eine 422-Antwort mit den Fehlern, bevor die Methode überhaupt ausgeführt wird.

Eine StoreUserRequest:


<?php

namespace App\Http\Requests;

use Illuminate\Foundation\Http\FormRequest;

class StoreUserRequest extends FormRequest

{

public function authorize(): bool

{

return $this->user()->can('create', User::class);

}

public function rules(): array

{

return [

'name'     => ['required', 'string', 'max:255'],

'email'    => ['required', 'email', 'unique:users,email'],

'password' => ['required', 'string', 'min:8'],

'role'     => ['required', 'in:admin,editor,viewer'],

];

}

}

Der Controller bleibt dünn. Genau das ist das Ziel: Thin Controllers von 10 bis 15 Zeilen, die nur noch orchestrieren. Die eigentliche Geschäftslogik wandert in eine Service-Klasse mit klarer Verantwortung. Der Controller nimmt den validierten Request entgegen, ruft den Service, gibt eine Resource zurück.


public function store(StoreUserRequest $request, UserService $service): UserResource

{

$user = $service->create($request->validated());

return new UserResource($user);

}

Vier Zeilen. Die Validierung steckt in der FormRequest, die Logik im UserService, die Antwortform in der UserResource. Jede Klasse hat einen Job. Wenn dieselbe Validierung später an einer zweiten Stelle gebraucht wird, ist sie schon ausgelagert und einsatzbereit.

Das TypeScript-Interface ist der Vertrag, nicht nur Doku

Jetzt die Frontend-Seite. Die UserResource hat oben eine konkrete JSON-Struktur definiert: id, name, email, role, createdAt. Genau diese Struktur bildet Angular als TypeScript-Interface ab. Das Interface ist der Vertrag zwischen Laravel-Resource und Angular-Frontend. Es ist nicht bloß Dokumentation, es ist eine vom Compiler durchgesetzte Garantie.


export interface User {

id: number;

name: string;

email: string;

role: 'admin' | 'editor' | 'viewer';

createdAt: string;

}

Beachten Sie die Spiegelung: createdAt heißt im Interface so wie im JSON, nicht created_at. Der role-String ist als Union-Typ modelliert und passt zur in:admin,editor,viewer-Regel der FormRequest. Wenn die Resource das Feld umbenennt, fällt die Diskrepanz im Frontend beim nächsten Build auf, sofern Sie das Interface mitziehen.

Der zentrale Punkt: kein any. Sobald eine API-Antwort als any durch den Code wandert, verlieren Sie jeden Schutz. Der Compiler fängt dann keine Tippfehler bei Attributnamen mehr ab, die Autovervollständigung schweigt, und niemand sieht der Variable noch an, welche Struktur sie trägt. Mit einem sauberen Interface dreht sich das um. Tippt jemand user.emial, meckert der Compiler sofort. Die IDE schlägt die Felder vor. Und ein Entwickler, der das Interface liest, versteht die Datenstruktur, ohne je in die Datenbank schauen zu müssen. Der Code dokumentiert sich selbst.

HttpClient gehört in einen Service, nicht in die Komponente

Der letzte Baustein ist der Datenzugriff in Angular. Hier gilt eine einfache Regel, die in der Praxis trotzdem oft verletzt wird: Der HttpClient gehört in eine dedizierte Service-Klasse, nicht in die Komponente. Komponenten kümmern sich um Darstellung und Interaktion. Wie die Daten geladen werden, ist nicht ihr Problem.


import { Injectable, inject } from '@angular/core';

import { HttpClient } from '@angular/common/http';

import { Observable } from 'rxjs';

import { User } from '../models/user.model';

@Injectable({ providedIn: 'root' })

export class UserService {

private http = inject(HttpClient);

private readonly baseUrl = '/api/users';

getUser(id: number): Observable<User> {

return this.http.get<User>(`${this.baseUrl}/${id}`);

}

createUser(payload: Omit<User, 'id' | 'createdAt'>): Observable<User> {

return this.http.post<User>(this.baseUrl, payload);

}

}

Das <User> an http.get und http.post ist der Moment, in dem der Vertrag greift. Angular typisiert die Antwort als User, und ab da arbeitet das ganze Frontend mit einem geprüften Typ. Der Omit<User, 'id' | 'createdAt'> beim Erstellen drückt sauber aus, dass Client id und createdAt nicht mitschickt, beides erzeugt der Server.

Rund um diesen Service kommen die üblichen Angular-Mechanismen. Interceptoren hängen Auth-Header an oder behandeln Fehler zentral. Guards schützen Routen. Welches Auth-Verfahren Sie dabei wählen, hängt vom Szenario ab. Für eine First-Party-SPA ist Laravel Sanctum mit Cookie-basierter Authentifizierung die naheliegende Wahl, weil httpOnly-Cookies den Token-Diebstahl per XSS verhindern. Die Details dazu stehen im Beitrag zur Laravel-Sanctum-Angular-Authentifizierung.

Bei den State-Aspekten lohnt ein Blick auf Angular Signals: Für lokale Zustände, Formularwerte und einfache Flags sind sie inzwischen das Mittel der Wahl und kommen ganz ohne manuelle Subscriptions aus. Für asynchrone Datenströme, wie sie der HttpClient liefert, bleibt RxJS relevant. Wer dort Observables im Template hält, nutzt die AsyncPipe oder takeUntilDestroyed, um Memory-Leaks zu vermeiden.

Der Datenfluss im Ganzen

Setzt man die Teile zusammen, ergibt sich ein klarer Weg. Eine Anfrage kommt vom Angular-Service über HttpClient. Die FormRequest validiert und autorisiert sie, bevor der Controller überhaupt arbeitet. Der dünne Controller ruft die Service-Klasse, die die Geschäftslogik trägt. Das Ergebnis geht durch die API Resource, die kontrolliert genau die Felder ausgibt, die der Vertrag vorsieht. Zurück im Frontend typisiert das User-Interface die Antwort, und der Compiler wacht über jeden Zugriff.

Jede Schicht hat eine Aufgabe, und keine kennt die internen Details der anderen mehr als nötig. Das ist der Sinn eines sauberen API-Vertrags: Sie können das Datenbankschema ändern, ohne die API zu brechen, und Sie können die API erweitern, ohne dass das Frontend rät. Aus meiner Projekterfahrung ist genau diese Disziplin der Unterschied zwischen einer Codebasis, die nach zwei Jahren noch wartbar ist, und einer, die niemand mehr anfassen will.

Sauberer API-Vertrag für Ihr Projekt

Ein durchdachter API-Vertrag aus Resources, FormRequests und spiegelnden Interfaces zahlt sich über die gesamte Lebensdauer eines Projekts aus. Wenn Sie eine geschäftskritische Angular-Laravel-Anwendung planen oder eine bestehende Codebasis aufräumen wollen, sprechen Sie mich an. Gemeinsam schauen wir, wo die Kopplung zwischen Backend und Frontend sauberer werden kann.

FAQ

Häufige Fragen

Brauche ich eine API Resource auch für kleine Projekte?

Sobald ein Eloquent-Modell als JSON nach außen geht, ja. Der Aufwand ist minimal, der Schutz vor versehentlichem Feld-Leak und vor unkontrollierten Schema-Änderungen aber sofort da. Bei einem reinen internen Prototyp ohne externe Konsumenten können Sie es lassen, aber sobald ein Angular-Frontend dranhängt, ist die Resource der saubere Weg.

Was ist der Unterschied zwischen FormRequest-Validierung und Validierung im Controller?

Funktional liefern beide dasselbe Ergebnis. Die FormRequest holt die Regeln aber aus dem Controller heraus an eine zentrale, wiederverwendbare Stelle und kümmert sich zusätzlich um die Autorisierung. Der Controller bleibt dünn und liest sich besser. Wird dieselbe Validierung an mehreren Stellen gebraucht, ist sie mit der FormRequest schon ausgelagert.

Muss ich das TypeScript-Interface manuell pflegen?

In der hier gezeigten Variante ja, das Interface spiegelt die Resource von Hand. Das ist überschaubar, solange die Strukturen klein sind, und gibt Ihnen volle Kontrolle. Wichtig ist nur die Disziplin: Ändert sich die Resource, ändern Sie das Interface mit. Wer die Generierung automatisieren will, kann Tooling einsetzen, das aus den Laravel-Resources Typen ableitet, der Vertragsgedanke bleibt derselbe.

Warum kein `any` für API-Antworten?

Weil any jeden Schutz abschaltet. Der Compiler prüft dann keine Attributnamen mehr, die Autovervollständigung fällt aus, und der Code verliert seine Selbstdokumentation. Ein konkretes Interface dreht das um: Tippfehler werden zur Build-Zeit gefangen, die IDE hilft aktiv, und die Datenstruktur ist allein aus dem Typ ersichtlich.

Wo gehört die Geschäftslogik hin, wenn nicht in den Controller?

In Service-Klassen mit klarer, einzelner Verantwortung. Der Controller orchestriert nur: Request entgegennehmen, Service rufen, Resource zurückgeben. Das hält ihn bei 10 bis 15 Zeilen und macht die Logik isoliert testbar und wiederverwendbar.

Kann ich denselben API-Vertrag auch mit GraphQL umsetzen?

Ja. Statt REST mit API Resources nutzen Sie dann Laravel Lighthouse im Backend und Apollo Angular im Frontend. Der Angular-spezifische HttpLink-Provider harmoniert nativ mit dem Angular HttpClient, unterstützt SSR und erlaubt klassische HTTP-Interceptoren. Das Vertragsdenken, typisierte Strukturen zwischen Backend und Frontend, bleibt dasselbe.

Nächster Schritt

Sollen wir Ihren KI-Use-Case einordnen?

Ich schaue mit Ihnen auf Ziel, Daten, Systeme und den sinnvollsten ersten Umsetzungsschritt.

Erstgespräch anfragen