Zum Inhalt springen
Abstrakte Formen aus Mattglas: ein massives Verbindungsstück vor nur angedeuteten Drahtformen

API-First: Warum die Schnittstelle vor dem Frontend entsteht

API-First-Entwicklung heißt, zuerst die Schnittstelle als Vertrag festzulegen und erst danach die Oberflächen darauf zu bauen. Die Endpunkte folgen dann den Begriffen des Fachgebiets statt einzelnen Bildschirmen, und jedes weitere Frontend nutzt dieselbe geprüfte Grundlage.

9 min Lesezeit Laravel & PHP

Wie das in der Praxis aussieht, zeigt eine REST-API für ein internes Tools-Portal, die wir für ein Münchner Startup gebaut haben. Der Auftrag umfasste eine REST-Schnittstelle für Anmeldung und Personaldisposition und zwei Frontends darauf: ein Backoffice für die Disponenten und ein schlankeres Portal für externe Mitarbeiter. Erst standen die Endpunkte, dann entstanden die Oberflächen. Die Schnittstelle war über ein Präfix in der URL versioniert, mit OpenAPI dokumentiert und auf Endpunkt-Ebene getestet. Über rund zehn Monate lag das Volumen bei rund 50 Stunden im Monat, danach übernahm das interne Team.

Die folgenden vier Schritte beschreiben dieses Vorgehen allgemein, mit einem kleinen Vertrag und einem passenden Test zum Nachbauen.

Prinzip

Was API-First bedeutet und was es verhindert

Bei API-First ist die Schnittstelle das erste Ergebnis eines Projekts. Oberflächen, Apps und Partnersysteme sind Clients, die diese Schnittstelle nutzen, und keiner von ihnen bestimmt allein, wie sie aussieht.

Der umgekehrte Weg ist verbreitet: Ein Bildschirm entsteht, und der Endpunkt dahinter liefert genau das, was dieser Bildschirm gerade braucht. Beim nächsten Bildschirm kommt ein ähnlicher Endpunkt mit anderen Feldnamen dazu. Im Tools-Portal haben wir deshalb die Reihenfolge umgedreht, denn in HR-Anwendungen ist das eine typische Falle: Die Endpunkte wachsen schief, weil sie der Logik der Oberfläche folgen statt der Domäne. Ein zweites Frontend erbt dann alle Sonderwege des ersten.

API-First legt dagegen drei Dinge vorher fest: die Begriffe, den Vertrag und die Tests, die den Vertrag prüfen. Welche Art von Schnittstelle dafür passt, ob REST, GraphQL oder ergänzende Webhooks, ist eine eigene Entscheidung; die Unterschiede stehen im Vergleich REST, GraphQL oder Webhooks. Die Beispiele unten nutzen REST.

Begriffe

Domäne vor Bildschirm

Am Anfang steht eine Liste der fachlichen Begriffe, noch ohne URL und ohne Bildschirm. Im Tools-Portal waren das Mitarbeiter, Schichten, Verfügbarkeiten und Buchungen, getrennt von der Anmeldung mit Login, Token-Erneuerung und Rollenprüfung. Für jeden Begriff klären Fachabteilung und Entwicklung:

  • Welche Felder gehören dazu, und welche sind Pflicht?

  • Welche Zustände gibt es, und welche Übergänge sind erlaubt? Eine Buchung ist zum Beispiel bestätigt oder storniert.

  • Wer darf anlegen, ändern, stornieren?

  • Welcher Begriff verweist auf welchen anderen?

Diese Liste ist die Grundlage der Endpunkte. Ein Endpunkt heißt dann /bookings, weil Buchungen ein Begriff des Fachgebiets sind, und nicht /calendar-week-data, weil ein Kalender sie gerade anzeigt. Wer die Methode dahinter vertiefen will, findet sie unter dem Stichwort Domain-Driven Design.

Hängt ein ERP oder eine Warenwirtschaft mit an der Anwendung, gehört deren Sicht in dieselbe Liste: Für jeden Begriff steht fest, welches System führt. Wie das bei einer Anbindung abläuft, steht in ERP-Schnittstelle anbinden.

Eine Steckbox, jede Öffnung passt zu genau einer Form, der orange Block senkt sich in sein passendes Loch.

Vertrag

Den API-Vertrag mit OpenAPI festhalten

Aus der Begriffsliste wird ein maschinenlesbarer Vertrag in der OpenAPI-Spezifikation. Er beschreibt für jeden Endpunkt die erlaubten Eingaben, die möglichen Statuscodes und den Aufbau jeder Antwort. Die OpenAPI Initiative veröffentlichte am 19.09.2025 die Versionen 3.1.2 und 3.2.0, am 10.09.2026 folgte 3.2.1. Version 3.1 übernimmt für Datenmodelle JSON Schema in der Fassung 2020-12. Version 3.2 ergänzt unter anderem die HTTP-Methode QUERY und Regeln für gestreamte Antworten. Das Beispiel kommt mit 3.1 aus.

Der Vertrag unten beschreibt einen einzigen Endpunkt: Ein angemeldeter Nutzer bucht einen Mitarbeiter für eine Schicht. Er legt fest:

  • Erfolg heißt Status 201 mit einem Objekt data, das genau fünf Felder enthält, keine weiteren (additionalProperties: false).

  • Der Status einer Buchung ist entweder confirmed oder cancelled.

  • Fehlende oder falsche Eingaben ergeben Status 422 mit message und errors, ohne Anmeldung kommt Status 401.

Die Version steckt wie im Fall des Tools-Portals im Pfad (/api/v1/). Wie man einen Vertrag später ändert, ohne bestehende Clients zu brechen, ist ein eigenes Thema.

openapi.yaml yaml
openapi: 3.1.2
info:
  title: Scheduling API (example)
  version: 1.0.0
paths:
  /api/v1/bookings:
    post:
      operationId: createBooking
      summary: Book an employee for a shift
      security:
        - bearerAuth: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/BookingInput'
      responses:
        '201':
          description: Booking created
          content:
            application/json:
              schema:
                type: object
                required: [data]
                additionalProperties: false
                properties:
                  data:
                    $ref: '#/components/schemas/Booking'
        '401':
          description: Not authenticated
        '422':
          description: Invalid input
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ValidationError'
components:
  securitySchemes:
    bearerAuth:
      type: http
      scheme: bearer
  schemas:
    BookingInput:
      type: object
      required: [shift_id, employee_id]
      properties:
        shift_id:
          type: integer
        employee_id:
          type: integer
    Booking:
      type: object
      required: [id, shift_id, employee_id, status, created_at]
      additionalProperties: false
      properties:
        id:
          type: integer
        shift_id:
          type: integer
        employee_id:
          type: integer
        status:
          type: string
          enum: [confirmed, cancelled]
        created_at:
          type: string
          format: date-time
    ValidationError:
      type: object
      required: [message, errors]
      properties:
        message:
          type: string
        errors:
          type: object
          additionalProperties:
            type: array
            items:
              type: string

Tests

Tests auf Endpunkt-Ebene

Endpunkt-Tests halten den Vertrag fest. Sie schicken echte HTTP-Anfragen an die Anwendung und vergleichen die Antwort mit dem, was der Vertrag verspricht. Laravel 13 bringt dafür die nötigen Prüfungen mit; das Beispiel unten ist mit Pest geschrieben, mit PHPUnit sieht es fast gleich aus. Jede Zeile des Tests gehört zu einer Zeile des Vertrags:

  • assertCreated prüft den Status 201.

  • assertExactJsonStructure prüft die fünf Pflichtfelder und schlägt fehl, sobald ein Feld fehlt oder ein zusätzliches auftaucht. Das entspricht required und additionalProperties: false.

  • assertJsonPath prüft, dass die gebuchte Schicht und der Mitarbeiter als Ganzzahl zurückkommen. whereType und die Prüfung auf confirmed oder cancelled decken Typen und erlaubte Werte ab.

  • assertUnprocessable mit assertJsonValidationErrors prüft die 422-Antwort, assertUnauthorized die 401-Antwort.

Nicht geprüft wird hier das Datumsformat von created_at; der Test stellt nur sicher, dass es eine Zeichenkette ist. Vorausgesetzt sind Modelle Shift und Employee mit Factories und eine Anmeldung über Laravel Sanctum.

Die strenge Strukturprüfung hat eine praktische Wirkung: Ergänzt jemand in der Antwort ein Feld, etwa eine interne Notiz, schlägt der Test fehl. Bevor die Änderung einen Client erreicht, steht damit die Frage im Raum, ob das Feld zum Vertrag gehören soll. Gehört es dazu, wird es in openapi.yaml und im Test ergänzt.

Eine Grenze hat dieser Test: Er bildet den Vertrag von Hand nach. Ändert jemand openapi.yaml und vergisst den Test, merkt der Test das nicht. Enger wird die Bindung, wenn die Tests jede Antwort zusätzlich automatisch gegen openapi.yaml prüfen lassen, mit einem OpenAPI-Validator für PHP. Achten Sie dabei auf die unterstützte Version: Der verbreitete league/openapi-psr7-validator, auf dem auch das Laravel-taugliche Paket osteel/openapi-httpfoundation-testing aufbaut, prüft laut eigener Beschreibung gegen OpenAPI 3.0. Für einen Vertrag in 3.1 braucht es ein Werkzeug mit 3.1-Unterstützung, oder der Vertrag wird in 3.0 geschrieben. Diese automatische Prüfung haben wir für das Beispiel hier nicht ausgeführt.

tests/Feature/CreateBookingTest.php php
<?php

use App\Models\Employee;
use App\Models\Shift;
use App\Models\User;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Testing\Fluent\AssertableJson;
use Laravel\Sanctum\Sanctum;

pest()->use(RefreshDatabase::class);

test('creates a booking as described in the contract', function () {
    Sanctum::actingAs(User::factory()->create());
    $shift = Shift::factory()->create();
    $employee = Employee::factory()->create();

    $response = $this->postJson('/api/v1/bookings', [
        'shift_id' => $shift->id,
        'employee_id' => $employee->id,
    ]);

    $response
        ->assertCreated()
        ->assertExactJsonStructure([
            'data' => ['id', 'shift_id', 'employee_id', 'status', 'created_at'],
        ])
        ->assertJsonPath('data.shift_id', $shift->id)
        ->assertJsonPath('data.employee_id', $employee->id)
        ->assertJson(fn (AssertableJson $json) => $json
            ->has('data', fn (AssertableJson $json) => $json
                ->whereType('id', 'integer')
                ->where('status', fn (string $status) => in_array($status, ['confirmed', 'cancelled'], true))
                ->whereType('created_at', 'string')
                ->etc()
            )
        );
});

test('rejects missing fields with 422', function () {
    Sanctum::actingAs(User::factory()->create());

    $this->postJson('/api/v1/bookings', [])
        ->assertUnprocessable()
        ->assertJsonStructure(['message', 'errors'])
        ->assertJsonValidationErrors(['shift_id', 'employee_id']);
});

test('rejects requests without a token with 401', function () {
    $this->postJson('/api/v1/bookings', ['shift_id' => 1, 'employee_id' => 1])
        ->assertUnauthorized();
});

Clients

Frontends gegen den Vertrag bauen

Steht der Vertrag, können Frontend und Backend parallel arbeiten. Aus der OpenAPI-Datei lassen sich Dokumentation, Typen für den Client und ein Testserver mit Beispielantworten erzeugen. Das Frontend-Team baut gegen diese Beispielantworten, während das Backend die Endpunkte umsetzt, und beide wissen, wann sie fertig sind: wenn die Tests aus Schritt 3 grün sind und der Client mit der echten Schnittstelle dieselben Daten sieht.

Im Tools-Portal liefen beide Frontends über dieselbe Anmeldeschicht, dieselbe Token-Erneuerung und dieselben Rollenprüfungen. Ein Fehler bei der Anmeldung war an einer Stelle zu beheben statt an zweien. Das interne Team konnte nach der Übergabe eigene Clients an die Schnittstelle anbinden, ohne die Umsetzung dahinter kennen zu müssen. Übergeben haben wir API-Dokumentation, Testsuite und Architecture Decision Records. Wie wir Schnittstellen dieser Art planen und liefern, beschreibt die Seite Schnittstellen und APIs.

Grenzen

Wann API-First übertrieben ist

API-First kostet am Anfang Zeit für Begriffe, Vertrag und Tests. Das zahlt sich aus, sobald mehrere Clients dieselben Daten nutzen oder ein externes Team anbinden soll. Es ist zu viel, wenn diese Bedingungen fehlen:

  • Die Anwendung hat genau eine Oberfläche, und es ist keine App und keine Partneranbindung absehbar. Mit Livewire oder Inertia braucht diese Oberfläche keine eigene, dokumentierte Schnittstelle; den Vergleich beider Wege zieht Livewire oder Vue.

  • Das Fachgebiet ist noch offen, etwa bei einem Prototyp, der erst zeigen soll, ob die Idee trägt. Ein Vertrag über Begriffe, die sich jede Woche ändern, erzeugt Pflegeaufwand ohne Nutzen.

  • Die Anwendung ist eine Website mit Inhalten und wenigen Formularen. Ob dort überhaupt eine Single Page App nötig ist, klärt der Beitrag Single Page App ist nicht immer die Lösung.

Schritt 1 bleibt trotzdem nützlich: Mit einer geklärten Begriffsliste lässt sich eine Schnittstelle später leichter nachrüsten.

Vertrag vor Code

Schreiben Sie für Ihr Vorhaben die Begriffsliste aus Schritt 1 auf, mit Feldern, Zuständen und Rechten. Wie aus so einer Liste eine REST-API mit zwei Frontends wurde, zeigt die Case Study.

Zur Case Study Tools-Portal

OpenAPI und Zeitplan

Häufige Fragen

  • Eine OpenAPI-Datei beschreibt eine HTTP-Schnittstelle maschinenlesbar in YAML oder JSON: Pfade, Methoden, Eingaben, Statuscodes, Antwortformate und Anmeldeverfahren. Pflicht sind die OpenAPI-Version, ein Info-Block mit Titel und Version sowie mindestens Pfade, Komponenten oder Webhooks. Aus der Datei entstehen Dokumentation, Client-Typen und Testserver, und Tests können die echte Schnittstelle dagegen prüfen.

  • Die erste sichtbare Oberfläche erscheint etwas später, weil Begriffe und Vertrag vorher stehen müssen. Dafür kann das Frontend-Team ab dem Vertrag parallel gegen Beispielantworten arbeiten, statt auf fertige Endpunkte zu warten. Im Projektverlauf entfällt außerdem das Nachziehen von Endpunkten, die nur für einzelne Bildschirme entstanden sind.

Nachweise

Quellen und Stand

Stand: 23.09.2026. Angaben zu OpenAPI laut spec.openapis.org, zu Tests laut Laravel-13-Dokumentation. Vertrag und Test sind ein Beispiel ohne Projektbezug; der Test setzt die genannten Modelle, Factories und Laravel Sanctum voraus.