
Perfekte RESTful API mit Laravel 12 & API Resources bauen

Kennst du diese Backend-Projekte, bei denen die Routen-Datei (web.php oder api.php) aussieht wie ein unendlicher, chaotischer Einkaufszettel? Früher habe ich für jeden noch so kleinen Endpunkt eine eigene Zeile geschrieben. Ein absoluter Albtraum für jeden, der diesen Code später warten musste. Damit machen wir jetzt Schluss. Um unser Headless CMS professionell an Next.js anzubinden, bauen wir eine echte, standardisierte Laravel REST API.
Keine ausgedachten URLs mehr, sondern präzise, vorhersehbare Endpunkte, die strikten architektonischen Regeln folgen. Dein Frontend-Team (auch wenn du das selbst bist) wird dir für eine saubere Schnittstelle ewig dankbar sein. Wir haben im vorherigen Teil unsere Datenbank relational perfekt aufgestellt. Jetzt ziehen wir die Kabel vom Tresorraum nach draußen.
Um unser Headless CMS professionell an Next.js anzubinden, brauchen wir eine echte, standardisierte Laravel REST API. Keine ausgedachten URLs, sondern präzise, vorhersehbare Endpunkte, die strikten architektonischen Regeln folgen. Dein Frontend-Team (auch wenn du das selbst bist) wird dir für eine saubere Schnittstelle ewig dankbar sein.
Wir haben im vorherigen Teil unsere Datenbank relational perfekt aufgestellt. Jetzt ziehen wir die Kabel vom Tresorraum nach draußen.
1. Die Kommandozentrale: Resource-Controller und sauberes Routing
Laravel bietet uns ein geniales Werkzeug, um die fünf Standard-Aktionen (Anzeigen, einzeln Aufrufen, Erstellen, Aktualisieren, Löschen) mit einem einzigen Befehl zu generieren. Wir nutzen dafür API-Ressourcen-Controller.
Öffne dein Terminal in unserem cms-backend Projekt und feuere diesen Befehl ab:
php artisan make:controller Api/PostController --apiDer Parameter --api ist hier der entscheidende Hebel. Wenn du dir den frisch generierten Controller unter app/Http/Controllers/Api/PostController.php ansiehst, wirst du feststellen, dass Laravel bewusst die Methoden create() und edit() weggelassen hat. Warum? Weil eine Laravel REST API keine HTML-Formulare ausliefert. Sie spricht reines JSON. Das Frontend (unser Next.js App Router) kümmert sich später um die Darstellung der Formulare.
Jetzt räumen wir unsere Routen auf. Öffne die Datei routes/api.php. Lösche alle alten, händischen Test-Routen, die wir bisher dort hatten, und ersetze sie durch diese einzige, mächtige Zeile:
1<?php
2
3use Illuminate\Support\Facades\Route;
4use App\Http\Controllers\Api\PostController;
5
6// Diese einzelne Zeile generiert sofort 5 perfekte REST-Routen
7Route::apiResource('posts', PostController::class);Was passiert hier im Hintergrund? Wenn du jetzt im Terminal php artisan route:list eingibst, siehst du die Magie. Laravel hat automatisch folgende Struktur für uns erschaffen:
GET /api/posts(index - Alle Artikel auflisten)POST /api/posts(store - Einen neuen Artikel speichern)GET /api/posts/{post}(show - Einen spezifischen Artikel anzeigen)PUT/PATCH /api/posts/{post}(update - Einen Artikel aktualisieren)DELETE /api/posts/{post}(destroy - Einen Artikel löschen)
Wir haben mit nur einer Zeile Code eine komplett standardisierte Schnittstelle hochgezogen. Keine Diskussionen mehr über URL-Benennungen.

2. Der unbestechliche Türsteher: Form Requests und sichere Validierung
Eine goldene Regel der Webentwicklung lautet: Vertraue niemals, unter keinen Umständen, den Daten, die von außen kommen. Wenn unser Next.js Frontend (oder ein böswilliger Bot) versucht, einen neuen Artikel über unsere Laravel REST API zu speichern, müssen wir diese Daten penibel prüfen. Fehlt der Titel? Ist der Slug schon vergeben? Enthält der Text schädliche Skripte?
Früher haben wir diese Prüfungen oft direkt in den Controller geschrieben. Das bläht den Code massiv auf und macht ihn unübersichtlich. Wir nutzen stattdessen Form Requests – quasi dedizierte Türsteher für unsere API, die den Schmutz abfangen, bevor er überhaupt in die Nähe unserer Datenbank kommt. Zudem nutzen wir direkt die modernen Validierungs-Features, die Laravel 12 uns bietet.
Gehe in dein Terminal und rekrutiere deinen ersten Türsteher für das Erstellen von Artikeln:
php artisan make:request StorePostRequestÖffne die frisch generierte Datei unter app/Http/Requests/StorePostRequest.php. Hier definieren wir die eisernen Gesetze für neue Artikel. Passe die Datei genau so an:
1<?php
2
3namespace App\Http\Requests;
4
5use Illuminate\Foundation\Http\FormRequest;
6
7class StorePostRequest extends FormRequest
8{
9 /**
10 * Bestimmt, ob der Nutzer diese Anfrage überhaupt stellen darf.
11 * (Wir setzen das vorerst auf true, das Rechtesystem kommt in Teil 5).
12 */
13 public function authorize(): bool
14 {
15 return true;
16 }
17
18 /**
19 * Die strikten Validierungsregeln für unsere Laravel REST API.
20 */
21 public function rules(): array
22 {
23 return [
24 // Der Titel ist Pflicht, muss ein String sein und max. 255 Zeichen haben
25 'title' => ['required', 'string', 'max:255'],
26
27 // Der Slug muss eindeutig (unique) in der 'posts' Tabelle sein
28 'slug' => ['required', 'string', 'max:255', 'unique:posts,slug'],
29
30 // Inhalt ist optional, aber wenn vorhanden, muss es ein String sein
31 'content' => ['nullable', 'string'],
32
33 // Status ist ein Boolean
34 'is_published' => ['boolean'],
35
36 // Ein Array von Kategorie-IDs, die in der Datenbank existieren müssen
37 'category_ids' => ['nullable', 'array'],
38 'category_ids.*' => ['exists:categories,id'],
39 ];
40 }
41}Schau dir die Regel für category_ids.* an. Mit diesem unscheinbaren Sternchen prüft Laravel automatisch jedes einzelne Element in einem mitgelieferten Array, ob die ID auch wirklich in unserer categories-Tabelle existiert. Ein absoluter Traum für relationale Daten!
Jetzt binden wir diesen Türsteher in unseren PostController ein. Öffne app/Http/Controllers/Api/PostController.php und erweitere die store-Methode:
1<?php
2
3namespace App\Http\Controllers\Api;
4
5use App\Http\Controllers\Controller;
6use App\Http\Requests\StorePostRequest;
7use App\Models\Post;
8use App\Http\Resources\PostResource;
9
10class PostController extends Controller
11{
12 // ... index() Methode bleibt bestehen ...
13
14 public function store(StorePostRequest $request)
15 {
16 // Wenn der Code diesen Punkt erreicht, sind die Daten zu 100% sicher und validiert!
17 // Wir können die sauberen Daten mit request()->validated() abrufen.
18 $validatedData = $request->validated();
19
20 // 1. Artikel speichern (wir setzen vorerst User ID 1 als Dummy-Autor)
21 $post = Post::create([
22 'title' => $validatedData['title'],
23 'slug' => $validatedData['slug'],
24 'content' => $validatedData['content'] ?? null,
25 'is_published' => $validatedData['is_published'] ?? false,
26 'user_id' => 1,
27 ]);
28
29 // 2. Kategorien verknüpfen, falls welche mitgesendet wurden
30 if (!empty($validatedData['category_ids'])) {
31 $post->categories()->attach($validatedData['category_ids']);
32 }
33
34 // 3. Den frisch erstellten Artikel als Resource formatieren und
35 // mit dem HTTP-Status 201 (Created) an das Frontend zurücksenden
36 return new PostResource($post->load(['author', 'categories']));
37 }
38}Wenn dein Next.js Frontend jetzt fehlerhafte Daten sendet, unterbricht Laravel den Vorgang sofort und schickt völlig automatisch eine saubere JSON-Antwort mit dem HTTP-Status 422 Unprocessable Entity und einer Liste aller Fehlermeldungen zurück. Dein Controller bleibt schlank und kümmert sich nur um die reine Geschäftslogik.

3. Dynamische Filter: So wird deine Laravel REST API intelligent
Eine API, die einfach nur stumpf alle Artikel aus der Datenbank wirft, ist schnell gebaut. Aber was passiert, wenn dein Next.js-Frontend auf der Startseite nur Artikel aus der Kategorie "Tech" anzeigen soll? Wenn wir alle 500 Artikel an den Browser senden und JavaScript die Sortierung überlassen, zwingen wir mobile Geräte gnadenlos in die Knie.
Die Lösung liegt im Backend. Eine professionelle Laravel REST API übernimmt die schwere Arbeit der Datenfilterung auf dem Server und liefert dem Client exakt nur das, was er angefordert hat.
Früher haben wir für so etwas oft gigantische if-else-Ketten im Controller gebaut. Das machen wir heute eleganter. Laravel bietet uns für den Eloquent ORM die fantastische when()-Methode an. Sie führt eine Datenbankabfrage nur dann aus, wenn eine bestimmte Bedingung (z. B. ein URL-Parameter) erfüllt ist.
Lass uns unseren PostController extrem flexibel machen. Öffne die Datei app/Http/Controllers/Api/PostController.php und passe die index-Methode wie folgt an:
1<?php
2
3namespace App\Http\Controllers\Api;
4
5use App\Http\Controllers\Controller;
6use App\Models\Post;
7use App\Http\Resources\PostResource;
8use Illuminate\Http\Request;
9
10class PostController extends Controller
11{
12 public function index(Request $request)
13 {
14 // Wir fangen die Query-Parameter aus der URL ab (z.B. ?category=tech & ?author_id=2)
15 $categorySlug = $request->query('category');
16 $authorId = $request->query('author_id');
17
18 $posts = Post::with(['author', 'categories'])
19 ->where('is_published', true)
20
21 // Filter 1: Nur anwenden, wenn '?category=' in der URL steht
22 ->when($categorySlug, function ($query, $categorySlug) {
23 // Wir suchen in der verknüpften Pivot-Tabelle nach dem Slug
24 $query->whereHas('categories', function ($q) use ($categorySlug) {
25 $q->where('slug', $categorySlug);
26 });
27 })
28
29 // Filter 2: Nur anwenden, wenn '?author_id=' in der URL steht
30 ->when($authorId, function ($query, $authorId) {
31 $query->where('user_id', $authorId);
32 })
33
34 ->latest()
35 ->paginate(10);
36
37 return PostResource::collection($posts);
38 }
39
40 // ... store() Methode bleibt unverändert ...
41}Schau dir diese Eleganz an! Dein Controller ist jetzt extrem mächtig, bleibt aber völlig lesbar. Wenn das Frontend nun die URL /api/posts?category=tech aufruft, erkennt die when()-Methode den Parameter und modifiziert die SQL-Abfrage dynamisch im Hintergrund. Rufst du /api/posts ohne Parameter auf, ignoriert Laravel die Blöcke einfach und liefert alle Artikel.
Du hast deine Schnittstelle soeben in ein intelligentes Suchwerkzeug verwandelt, ohne auch nur ein einziges unschönes if ($request->has('category')) schreiben zu müssen.

4. Der perfekte Daten-Vertrag: Fortgeschrittene API-Resources
Wir haben jetzt Endpunkte, die dynamisch filtern, und Türsteher, die fehlerhafte Daten gnadenlos abblocken. Doch was genau senden wir eigentlich zurück?
Wenn wir einfach $post->toArray() an unser Frontend schicken, begehen wir eine architektonische Todsünde. Wir koppeln unsere interne Datenbankstruktur direkt an das externe UI. Ändert sich morgen dein Spaltenname von content zu body, bricht dein komplettes Next.js-Frontend mit einem lauten Knall zusammen.
Eine exzellente Laravel REST API nutzt einen strikten Daten-Vertrag: die API-Resource. Wir haben sie in Teil 2 bereits kurz kennengelernt, um das N+1 Problem zu lösen. Jetzt heben wir sie auf Enterprise-Niveau. Wir wollen unserem Frontend nicht nur die nackten Daten liefern, sondern auch berechnete Felder (Computed Properties) und bedingte Attribute, die nur bestimmte Nutzer sehen dürfen.
Öffne deine bestehende app/Http/Resources/PostResource.php. Wir bauen jetzt Intelligenz in diesen Übersetzer ein:
1<?php
2
3namespace App\Http\Resources;
4
5use Illuminate\Http\Request;
6use Illuminate\Http\Resources\Json\JsonResource;
7
8class PostResource extends JsonResource
9{
10 public function toArray(Request $request): array
11 {
12 return [
13 'id' => $this->id,
14 'title' => $this->title,
15 'slug' => $this->slug,
16
17 // Wir berechnen die geschätzte Lesezeit direkt auf dem Server!
18 'reading_time_minutes' => ceil(str_word_count(strip_tags($this->content)) / 200),
19
20 'excerpt' => str()->limit($this->content, 120),
21 'content' => $this->content,
22
23 // Dieses Feld wird NUR mitgeschickt, wenn der Request von einem Admin kommt
24 'internal_notes' => $this->when($request->user()?->isAdmin(), $this->internal_notes),
25
26 'published_at' => $this->created_at->format('d.m.Y'),
27
28 // Relationen, die nur geladen werden, wenn sie explizit angefordert wurden
29 'author' => new UserResource($this->whenLoaded('author')),
30 'categories' => CategoryResource::collection($this->whenLoaded('categories')),
31 ];
32 }
33
34 /**
35 * Fügt jedem einzelnen Item oder der gesamten Collection
36 * automatisch zusätzliche Meta-Daten hinzu.
37 */
38 public function with(Request $request): array
39 {
40 return [
41 'meta' => [
42 'api_version' => '1.0.0',
43 'author_url' => url('https://webinteger.dev')
44 ],
45 ];
46 }
47}Schau dir die Methode with() am Ende an. Egal, ob wir einen einzelnen Artikel oder eine Liste von 50 Artikeln abrufen, unsere Laravel REST API hängt jetzt völlig automatisch diese Meta-Informationen in das JSON-Objekt. Dein Frontend kann diese Daten nutzen, um beispielsweise zu prüfen, ob es noch mit der korrekten API-Version spricht.
Und das Feld reading_time_minutes? Das ist pure Magie. Anstatt dass das Smartphone des Nutzers rechenintensive JavaScript-Operationen durchführen muss, um Wörter zu zählen, übernimmt unser leistungsstarker Server diese Arbeit im Bruchteil einer Millisekunde und liefert das fertige Ergebnis. Das ist der Unterschied zwischen einer guten und einer perfekten Schnittstelle.

5. Der Lebenszyklus schließt sich: Update und Delete Methoden
Unsere Laravel REST API kann jetzt Artikel ausliefern, filtern und neue Daten extrem sicher anlegen. Um den vollen REST-Standard (Representational State Transfer) zu erfüllen, müssen wir unseren Controller noch um die Fähigkeiten zur Bearbeitung (PUT/PATCH) und zum Löschen (DELETE) erweitern.
Dank des Laravel Route Model Bindings ist das ein absolutes Kinderspiel. Wenn das Frontend einen Request an /api/posts/14 schickt, sucht Laravel im Hintergrund bereits automatisch den Artikel mit der ID 14. Findet es ihn nicht, bricht es mit einem sauberen 404-Fehler ab, noch bevor unser Controller-Code überhaupt erreicht wird.
Lass uns zuerst einen neuen Türsteher für das Update erschaffen, denn beim Bearbeiten gelten leicht abgewandelte Regeln. Oft möchte man nur den Titel ändern, ohne den Inhalt mitsenden zu müssen:
php artisan make:request UpdatePostRequestÖffne die Datei app/Http/Requests/UpdatePostRequest.php und passe sie so an:
1<?php
2
3namespace App\Http\Requests;
4
5use Illuminate\Foundation\Http\FormRequest;
6use Illuminate\Validation\Rule;
7
8class UpdatePostRequest extends FormRequest
9{
10 public function authorize(): bool
11 {
12 return true; // Später prüfen wir hier, ob der User der Autor ist
13 }
14
15 public function rules(): array
16 {
17 return [
18 // Sometimes bedeutet: Nur validieren, wenn das Feld auch mitgeschickt wurde
19 'title' => ['sometimes', 'required', 'string', 'max:255'],
20
21 // Der Slug muss unique sein, DARF aber dem aktuellen Artikel gehören!
22 'slug' => [
23 'sometimes',
24 'required',
25 'string',
26 'max:255',
27 Rule::unique('posts')->ignore($this->post)
28 ],
29
30 'content' => ['nullable', 'string'],
31 'is_published' => ['sometimes', 'boolean'],
32 'category_ids' => ['sometimes', 'array'],
33 'category_ids.*' => ['exists:categories,id'],
34 ];
35 }
36}Die Regel Rule::unique('posts')->ignore($this->post) ist ein geniales Stück Laravel REST API Magie. Sie verhindert, dass die Validierung fehlschlägt, weil der Slug ja logischerweise schon in der Datenbank existiert – nämlich bei exakt dem Artikel, den wir gerade bearbeiten wollen.
Jetzt vervollständigen wir unseren PostController:
1<?php
2
3namespace App\Http\Controllers\Api;
4
5use App\Http\Controllers\Controller;
6use App\Http\Requests\StorePostRequest;
7use App\Http\Requests\UpdatePostRequest;
8use App\Models\Post;
9use App\Http\Resources\PostResource;
10use Illuminate\Http\Request;
11
12class PostController extends Controller
13{
14 // ... index() und store() bleiben unverändert ...
15
16 /**
17 * Einen einzelnen Artikel abrufen (GET /api/posts/{post})
18 */
19 public function show(Post $post)
20 {
21 // Wir laden die Relationen nach
22 $post->load(['author', 'categories']);
23 return new PostResource($post);
24 }
25
26 /**
27 * Einen Artikel aktualisieren (PUT/PATCH /api/posts/{post})
28 */
29 public function update(UpdatePostRequest $request, Post $post)
30 {
31 $validatedData = $request->validated();
32
33 // Nur die Felder aktualisieren, die mitgeschickt wurden
34 $post->update($validatedData);
35
36 // Kategorien synchronisieren (alte entfernen, neue setzen)
37 if (isset($validatedData['category_ids'])) {
38 $post->categories()->sync($validatedData['category_ids']);
39 }
40
41 return new PostResource($post->load(['author', 'categories']));
42 }
43
44 /**
45 * Einen Artikel löschen (DELETE /api/posts/{post})
46 */
47 public function destroy(Post $post)
48 {
49 $post->delete();
50
51 // Ein 204 (No Content) Statuscode ist der REST-Standard für erfolgreiches Löschen
52 return response()->noContent();
53 }
54}Schau dir die destroy() Methode an. Da wir im vorherigen Kapitel (Teil 2) "Soft Deletes" eingerichtet haben, wird der Artikel durch $post->delete() nicht physisch zerstört, sondern lediglich als gelöscht markiert. Dein Sicherheitsnetz greift vollautomatisch.

Zusammenfassung: Deine Laravel REST API ist einsatzbereit
Wir haben die Nabelschnur zwischen Datenbank und Frontend erfolgreich durchtrennt und durch ein hochprofessionelles, normiertes System ersetzt. Deine Laravel REST API ist nun strikt nach den REST-Prinzipien strukturiert. Ein einziger Resource-Controller verwaltet elegant alle fünf Endpunkte.
Du hast gelernt, wie man einkommende Daten durch Form Requests unbestechlich validiert, wie man Datenbankabfragen durch when()-Filter dynamisch und ressourcenschonend hält und wie man die rohen Datenbankmodelle mithilfe von API-Resources in verlässliche, versionssichere JSON-Verträge verwandelt. Dein Next.js Frontend kann sich jetzt blind auf das Format und die Sicherheit der gelieferten Daten verlassen.
Teil der Serie
Headless CMS mit Laravel und Next.js
Headless CMS mit Laravel 12 & Next.js: Der ultimative Guide Pillar
Das perfekte Laravel Next.js Setup: Projektstruktur für dein Headless CMS
Datenbank-Design für ein skalierbares Headless CMS
Perfekte RESTful API mit Laravel 12 & API Resources bauen
Sichere Laravel Sanctum Next.js Authentifizierung bauen
Häufig gestellte Fragen (FAQ)
Ausblick auf Teil 4: Die Festung absichern mit Laravel Sanctum
Unsere API ist aktuell ein offenes Buch. Jeder, der die URL kennt, kann im Moment Artikel erstellen, verändern oder löschen. Das ist für ein Headless CMS natürlich ein fataler Zustand. Im nächsten Teil unserer Masterclass stürzen wir uns auf den Endgegner der modernen Webentwicklung: Die Authentifizierung zwischen zwei entkoppelten Systemen.
Wir werden Laravel Sanctum implementieren, um unser Next.js Frontend absolut kugelsicher an das Backend zu binden. Wir verabschieden uns von unsicheren LocalStorage-Hacks und etablieren den Austausch von unsichtbaren, HttpOnly-Cookies. Du wirst lernen, wie CORS und Sanctum Hand in Hand arbeiten, um SPA-Authentifizierung so geschmeidig und sicher wie möglich zu machen.
Hier geht es zu Teil 4: Sichere SPA-Authentifizierung mit Laravel Sanctum & Next.js

Dietrich Bojko
Senior Webentwickler
Webinteger arbeitet seit vielen Jahren produktiv mit
Linux-basierten Entwicklungsumgebungen unter Windows.
Der Fokus liegt auf
performanten Setups mit WSL 2, Docker, PHP, Node.js und modernen
Build-Tools in realen Projekten –
nicht auf theoretischen Beispielkonfigurationen.
Die Artikel dieser Serie entstehen direkt aus dem täglichen Einsatz in Kunden- und Eigenprojekten und dokumentieren bewusst auch typische Fehler, Engpässe und bewährte Workarounds.


