{ }
GET
REST API · Backoffice · Public API · Symfony · PHP
REST APIs für Backoffice vs. Public
unterschiedlich entwerfen

Ein Backoffice-API und eine Public API teilen HTTP als Transport – aber ihre Anforderungen an Stabilität, Abwärtskompatibilität, Sicherheit und Fehlermodelle unterscheiden sich so fundamental, dass ein einheitliches Design zwangsläufig eines von beidem schlechter macht.

12 Min. Lesezeit Authentication · Versionierung · Rate Limiting · Fehlermodelle Symfony 7 · PHP 8.4 · REST

1. Warum Backoffice und Public APIs grundsätzlich verschieden sind

Der häufigste Fehler beim API-Design ist, eine einzige API zu entwerfen, die sowohl internen Backoffice-Anforderungen als auch externen Entwicklern dient. Das klingt nach DRY-Prinzip, führt in der Praxis aber zu einem schlechten Kompromiss für beide Seiten. Eine Backoffice-API bedient bekannte, kontrollierte Clients – typischerweise ein internes Admin-Panel, ein ERP-System oder eine mobile App, die vom eigenen Team entwickelt wird. Eine Public API muss mit unbekannten Clients, undokumentierten Nutzungsmustern und einer Vielzahl von Programmiersprachen und Plattformen umgehen, die sich niemand vollständig vorstellen kann.

Der zweite wesentliche Unterschied liegt in der Änderungshoheit. Bei einer Backoffice-API kann ein Team entscheiden: „Wir ändern diesen Endpoint morgen, alle Clients werden gleichzeitig aktualisiert." Bei einer Public API ist das unmöglich. Externe Entwickler vertrauen auf Stabilität, haben eigene Deployment-Zyklen und können nicht innerhalb von 24 Stunden auf eine API-Änderung reagieren. Diese unterschiedliche Änderungsdynamik zieht sich durch alle folgenden Designentscheidungen und ist der Grund, warum eine einheitliche API für beide Use Cases scheitert.

2. Zielgruppe als primärer Designtreiber

Jede gute API-Designentscheidung beginnt mit der Frage: „Wer ruft diesen Endpoint auf, und was braucht er wirklich?" Für eine Backoffice-API sind die Antworten präzise: Der Aufrufer ist ein bekanntes internes System mit definierten Rechten, er benötigt oft detailreichere Daten als ein externer Konsument und er toleriert komplexere Request-Strukturen, weil das aufrufende Team dieselbe Fachsprache spricht. Ein Backoffice-Endpoint für Bestellungsdetails kann ohne Weiteres auch interne Daten wie Lagerort, Einkaufspreis und interne Notizen zurückgeben.

Eine Public API hingegen muss datenschutzrechtliche Aspekte berücksichtigen und darf interne Daten niemals in Responses durchsickern lassen. Die Zielgruppe sind externe Entwickler, die möglicherweise nie ein persönliches Gespräch mit dem API-Designteam hatten. Response-Felder müssen selbsterklärend sein, Fehlermeldungen müssen verständlich sein, ohne interne Systemkenntnisse zu erfordern, und die Dokumentation muss vollständig sein, weil kein Slack-Channel zum Nachfragen existiert. Diese kognitiven Anforderungen formen das gesamte API-Design von Grund auf.


<?php
// Backoffice API Controller — rich internal data, no public exposure
#[Route('/api/backoffice/orders/{id}', methods: ['GET'])]
public function getOrderDetail(
    int $id,
    OrderRepository $orders,
    #[CurrentUser] User $admin,
): JsonResponse {
    $this->denyAccessUnlessGranted('ROLE_BACKOFFICE');

    $order = $orders->findWithInternalDetails($id);

    return $this->json([
        'id'              => $order->getId(),
        'status'          => $order->getStatus()->value,
        'customer_email'  => $order->getCustomer()->getEmail(),
        'purchase_price'  => $order->getInternalPurchasePrice(), // internal only
        'margin_percent'  => $order->getMarginPercent(),         // internal only
        'warehouse_slot'  => $order->getWarehouseSlot(),         // internal only
        'internal_notes'  => $order->getInternalNotes(),         // internal only
        'created_at'      => $order->getCreatedAt()->format('c'),
    ]);
}

// Public API Controller — filtered, safe, documented fields only
#[Route('/api/v1/orders/{id}', methods: ['GET'])]
public function getPublicOrderDetail(
    int $id,
    OrderRepository $orders,
    Security $security,
): JsonResponse {
    $order = $orders->findPublicById($id);

    // Ensure only the owner can access
    if ($order->getCustomerId() !== $security->getUser()?->getId()) {
        throw new AccessDeniedException();
    }

    return $this->json([
        'id'         => $order->getId(),
        'status'     => $order->getStatus()->value,
        'items'      => $this->serializeItems($order->getItems()),
        'total'      => $order->getTotal(),
        'created_at' => $order->getCreatedAt()->format('c'),
        // No internal fields, no pricing details, no warehouse data
    ]);
}

3. Authentication: Session vs. API-Key vs. OAuth2

Authentication ist einer der klarsten Bereiche, in denen Backoffice- und Public APIs unterschiedliche Lösungen brauchen. Eine Backoffice-API, die nur vom eigenen Admin-Panel aufgerufen wird, kann problemlos mit Session-Cookies oder einem einfachen Bearer-Token arbeiten, der an ein internes User-Objekt gebunden ist. Das ist einfach zu implementieren, einfach zu widerrufen und bietet vollständige Rückverfolgbarkeit, weil jede Anfrage einem konkreten Admin-Benutzer zugeordnet werden kann. Symfony's Security-Komponente mit einem internen User-Provider und `ROLE_BACKOFFICE` ist ausreichend und verursacht keinen unnötigen Overhead.

Eine Public API für externe Entwickler benötigt einen anderen Ansatz. API-Keys mit scoped Permissions sind das Minimum: Jeder externe Entwickler erhält einen Key, der bestimmte Scopes hat (`orders:read`, `products:write`) und jederzeit widerrufbar ist, ohne den User-Account zu löschen. Für APIs, die im Namen von End-Benutzern agieren, ist OAuth2 mit Authorization Code Flow die richtige Wahl. Das gibt End-Benutzern Kontrolle darüber, welche Anwendungen auf ihre Daten zugreifen dürfen, und entspricht dem Erwartungsstandard moderner öffentlicher APIs. Beide Modelle existieren nebeneinander, aber in einer öffentlichen API gibt es keinen Grund, Session-Authentication anzubieten.

4. Versionierung: wann notwendig, wann kontraproduktiv

Backoffice-APIs brauchen selten eine formale Versionierungsstrategie. Wenn alle Clients unter der eigenen Kontrolle stehen und gleichzeitig aktualisiert werden können, ist eine `/api/backoffice/` URL-Struktur ohne Versionsnummer einfacher zu warten. Breaking Changes sind koordinierbar, und das Mitschleppen mehrerer API-Versionen kostet Wartungsaufwand ohne proportionalen Nutzen. Die einzige Ausnahme: Wenn mehrere interne Systeme mit eigenem Deployment-Zyklus dieselbe Backoffice-API konsumieren, braucht es zumindest eine grobe Versionierungsstrategie.

Public APIs hingegen brauchen Versionierung als Stabilitätsversprechen. `/api/v1/` in der URL ist das am weitesten verbreitete Muster und hat den Vorteil, dass es für Entwickler sofort sichtbar ist. Die Alternative – Versionierung über Accept-Header (`Accept: application/vnd.mironsoft.v2+json`) – ist semantisch sauberer, aber schwerer zu testen und zu dokumentieren. Wichtig: Eine neue API-Version bedeutet nicht, dass die alte sofort abgeschaltet wird. Ein Deprecation-Zeitraum von mindestens 12 Monaten mit klaren Deprecation-Warnings im Response-Header (`Sunset: Sat, 31 Dec 2027 23:59:59 GMT`) ist guter API-Bürgersinn.


<?php
// Symfony API version detection via URL prefix
// config/routes/api_public.yaml:
// api_v1:
//   resource: '../src/Controller/Api/V1/'
//   prefix: /api/v1
//
// api_v2:
//   resource: '../src/Controller/Api/V2/'
//   prefix: /api/v2

// EventSubscriber: inject deprecation headers for v1 responses
use Symfony\Component\EventDispatcher\Attribute\AsEventListener;
use Symfony\Component\HttpKernel\Event\ResponseEvent;
use Symfony\Component\HttpKernel\KernelEvents;

#[AsEventListener(event: KernelEvents::RESPONSE)]
final class ApiDeprecationSubscriber
{
    public function __invoke(ResponseEvent $event): void
    {
        $path = $event->getRequest()->getPathInfo();

        if (str_starts_with($path, '/api/v1/')) {
            $response = $event->getResponse();
            $response->headers->set('Deprecation', 'true');
            $response->headers->set('Sunset', 'Sat, 31 Dec 2027 23:59:59 GMT');
            $response->headers->set('Link', '</api/v2/>; rel="successor-version"');
        }
    }
}

5. Rate Limiting und Throttling

Rate Limiting ist für Public APIs existenziell und für Backoffice-APIs optional. Eine Public API muss sich gegen unbeabsichtigte und beabsichtigte Überlastung schützen. Ein externer Entwickler, der versehentlich eine Endlosschleife baut, kann ohne Rate Limiting die gesamte Infrastruktur beeinflussen. Das übliche Modell: Anfragen pro API-Key und Zeitfenster begrenzen, die Limits in Response-Headern kommunizieren (`X-RateLimit-Limit`, `X-RateLimit-Remaining`, `X-RateLimit-Reset`) und bei Überschreitung mit HTTP 429 und einem `Retry-After`-Header antworten. Symfony's RateLimiter-Komponente implementiert Token Bucket und Sliding Window Algorithmen ohne externe Abhängigkeiten.

Backoffice-APIs profitieren von Rate Limiting als Schutz gegen Fehler in internen Skripten, aber die Limits können deutlich großzügiger sein. Eine interne Batch-Operation, die 10.000 Datensätze importiert, darf nicht durch ein zu enges Rate Limit blockiert werden. Der sinnvollere Ansatz für Backoffice-APIs ist Connection Pooling auf Datenbankebene und Circuit Breaker für abhängige externe Services, nicht künstliche Limits auf HTTP-Ebene. Die Unterscheidung „Wer ruft das auf, und welche Lastmuster sind erwartet?" führt zu sinnvolleren Limits als eine einheitliche Regel für alle Endpoints.

6. Fehlermodelle und Response-Granularität

Das Fehlermodell ist ein weiterer Bereich mit tiefgreifenden Unterschieden. Eine Backoffice-API kann bei einem Fehler ausführliche technische Details zurückgeben: Stack-Trace, SQL-Query, interne Exception-Message. Das ist für Debugging in der Entwicklungsphase wertvoll und für interne Teams akzeptabel, weil kein Risiko besteht, dass die Informationen öffentlich werden. Eine Public API darf niemals Stack-Traces, SQL-Fehler oder interne Systempfade in Fehlerresponses zurückgeben – das ist sowohl ein Sicherheitsrisiko als auch eine schlechte Developer Experience.

Für Public APIs empfiehlt sich RFC 9457 (Problem Details for HTTP APIs): Ein standardisiertes JSON-Objekt mit `type`, `title`, `status`, `detail` und optionalen Extensions. Externe Entwickler kennen dieses Format, können es automatisch parsen und erhalten konsistente Fehlerbeschreibungen ohne interne Systemdetails. Validierungsfehler bei mehreren Feldern sollten vollständig zurückgegeben werden (`errors`-Array), nicht nur der erste aufgetretene Fehler. Ein Entwickler, der vier Felder falsch befüllt hat und vier API-Aufrufe braucht, um alle Fehler zu entdecken, wird keine positive Developer Experience haben.

7. Pagination und Filterstrategie

Pagination ist in beiden API-Typen notwendig, aber mit unterschiedlichen Strategien. Backoffice-APIs für Admin-Panels können Offset-basierte Pagination verwenden (`?page=2&per_page=50`), weil der Datenbestand kontrolliert ist und der Anwendungsfall typischerweise keine Hochvolumen-Streaming-Anforderungen hat. Cursor-basierte Pagination ist performanter bei großen Datensätzen, erfordert aber eine andere Client-Implementierung und ist für einfache Admin-Interfaces oft überdimensioniert.

Public APIs, die von Entwicklern für alle möglichen Anwendungsfälle genutzt werden, sollten cursor-basierte Pagination anbieten. Wenn jemand eine Produkt-Liste mit 500.000 Einträgen durchiteriert, ist Offset-Pagination nicht nur langsam, sondern produziert auch inkonsistente Ergebnisse bei gleichzeitigen Schreiboperationen (ein Datensatz kann übersprungen oder doppelt erscheinen). Cursor-basierte Pagination liefert deterministisch vollständige Ergebnisse. Die Cursor-Informationen gehören in einen `meta`-Block des Response-Bodys, nicht in HTTP-Header, weil sie komplexe Strukturen sein können.

Aspekt Backoffice API Public API Begründung
Authentication Session / internes JWT API-Key + OAuth2 Kontrollierter vs. unbekannter Client
Versionierung Optional / koordiniert URL-Prefix /v1/ Pflicht Eigene vs. fremde Deployment-Zyklen
Rate Limiting Großzügig / Circuit Breaker Strikt per API-Key Bekannte vs. unbekannte Lastmuster
Fehlerdetails Stack-Trace erlaubt RFC 9457 Problem Details Intern nützlich vs. extern sicher
Pagination Offset-basiert ausreichend Cursor-basiert bevorzugt Kontrollierbares vs. beliebiges Volumen

8. Stabilität und Breaking Changes

Das Thema Stabilität ist vielleicht der fundamentalste Unterschied im API-Design. Eine Backoffice-API kann Breaking Changes einführen, solange alle Clients koordiniert aktualisiert werden. Das erleichtert iterative Entwicklung erheblich: Ein neues Datenmodell, ein umbenanntes Feld, ein geändertes Status-Format – das sind bei einer internen API Refactorings, keine API-Designprobleme. Das Team hat die volle Kontrolle über den Migrationszeitraum und kann im Zweifelsfall einen Big Bang Deploy durchführen.

Public APIs dagegen tragen die Last der Abwärtskompatibilität als primäre Designanforderung. Ein Feld, das einmal in einer Response erscheint, darf nicht einfach entfernt werden. Eine Umbenennung erfordert einen Deprecation-Prozess, bei dem das alte Feld noch mindestens 12 Monate parallel zur neuen Bezeichnung zurückgegeben wird. Neue optionale Felder in Responses sind backward-compatible. Neue Pflichtfelder in Requests sind es nicht. Diese Asymmetrie verlangt von API-Designern, von Anfang an in Stabilitätskategorien zu denken und Felder bewusst als stabil oder experimentell zu markieren.


<?php
// Symfony EventSubscriber: log deprecated field usage from public API clients
use Symfony\Component\EventDispatcher\Attribute\AsEventListener;
use Symfony\Component\HttpKernel\Event\RequestEvent;
use Psr\Log\LoggerInterface;

#[AsEventListener]
final class DeprecatedFieldUsageListener
{
    public function __construct(
        private readonly LoggerInterface $logger,
    ) {}

    public function onKernelRequest(RequestEvent $event): void
    {
        $request = $event->getRequest();
        if (!str_starts_with($request->getPathInfo(), '/api/v1/')) {
            return;
        }

        // Track if client sends old field name
        $body = json_decode($request->getContent(), true) ?? [];

        if (isset($body['customer_name'])) {
            // 'customer_name' was renamed to 'full_name' in v1.3
            $this->logger->warning('Deprecated field used: customer_name', [
                'api_key'  => $request->headers->get('X-Api-Key', 'unknown'),
                'endpoint' => $request->getPathInfo(),
            ]);
        }
    }
}

Mironsoft

REST API Design, Symfony Backend-Entwicklung und API-Strategie

REST APIs, die für Ihr konkretes Anwendungsszenario entworfen werden?

Wir entwerfen und implementieren Backoffice- und Public APIs mit Symfony, die von Anfang an die richtigen Prioritäten setzen – Stabilität, Sicherheit und Developer Experience abgestimmt auf Ihre spezifischen Client-Anforderungen.

API-Audit

Bestehende APIs analysieren, Sicherheitslücken und Designfehler identifizieren

API-Design

Neue APIs von Grund auf entwerfen – mit klarer Backoffice/Public-Trennung

Migration

Bestehende APIs in v1/v2 aufteilen mit Deprecation-Strategie und Client-Kommunikation

10. Zusammenfassung

Die zentrale Erkenntnis dieses Artikels: Backoffice-APIs und Public APIs teilen HTTP als Transport, aber ihre Designanforderungen sind grundlegend verschieden. Eine Backoffice-API darf reich an internen Details sein, kann auf formale Versionierung verzichten und profitiert von ausführlichen Fehlermeldungen für Debugging. Eine Public API braucht strikte Datentrennung, formale Versionierung als Stabilitätsversprechen, standardisierte Fehlermodelle nach RFC 9457 und Rate Limiting als Schutz für alle Beteiligten.

Das Gegenstück zur Designentscheidung ist die organisatorische Konsequenz: Wer dieselbe API für interne und externe Konsumenten baut, optimiert für keinen der beiden Anwendungsfälle. Die Auftrennung in separate Endpunkte oder gar separate Services ist initial mehr Aufwand, zahlt sich aber durch geringere Wartungskomplexität aus. Eine Backoffice-API, die wegen Public-API-Anforderungen keine Stack-Traces zurückgeben darf, ist für internen Betrieb schlechter. Eine Public API, die wegen Backoffice-Anforderungen interne Felder zurückgibt, ist ein Sicherheitsrisiko.

Backoffice vs. Public API — Das Wichtigste auf einen Blick

Backoffice API

Interne Clients, koordinierbare Breaking Changes, ausführliche Fehlermeldungen, großzügiges Rate Limiting. Kein Versionierungszwang, kein OAuth2-Overhead.

Public API

URL-Versionierung (/v1/), API-Keys + OAuth2, RFC 9457 Fehlermodell, striktes Rate Limiting, Cursor-Pagination, 12 Monate Deprecation-Zeitraum.

Datentrennung

Interne Felder (Einkaufspreis, Lagerort, Notizen) niemals in Public-API-Responses – verschiedene Serialisierungsschichten erzwingen das.

Stabilität

Public API: additive Änderungen sind safe, Breaking Changes erfordern neue Version + Sunset-Header. Backoffice: koordinierter Big Bang möglich.

11. FAQ: REST APIs für Backoffice vs. Public

1Muss ich wirklich zwei separate APIs bauen?
Nicht zwingend zwei Deployments, aber separate Routing-Präfixe mit separaten Controllern und Serialisierungsschichten. Das erzwingt Datentrennung ohne doppelte Infrastruktur.
2Warum keine internen Felder in Public APIs?
Datenschutz und ungewollte Abhängigkeiten. Externe Entwickler bauen auf zurückgegebene Felder, und ein später entferntes internes Feld wird zum Breaking Change.
3Beste Versionierungsstrategie für Public APIs?
URL-Prefix /api/v1/ ist am weitesten verbreitet. Sunset-Header + 12 Monate Deprecation-Zeit bei Breaking Changes.
4Rate Limiting in Symfony implementieren?
symfony/rate-limiter mit Token Bucket oder Sliding Window. Limits per API-Key, X-RateLimit-* Header setzen, HTTP 429 mit Retry-After bei Überschreitung.
5Was ist RFC 9457 Problem Details?
IETF-Standard für maschinenlesbare Fehlerresponses. JSON mit type, title, status, detail und optionalen Extensions für Feldfehler oder Correlation IDs.
6Cursor vs. Offset Pagination?
Cursor-basiert bei großen Daten mit parallelen Schreiboperationen. Offset-Pagination kann bei gleichzeitigen Änderungen Datensätze überspringen oder doppelt liefern.
7Breaking Changes kommunizieren?
Sunset-Header, Deprecation-Header (RFC 8594), E-Mail an registrierte Entwickler, Changelog und Migrationsanleitung. Alle Kanäle, mindestens 12 Monate Vorlauf.
8Stack-Traces in Backoffice-API erlaubt?
In Entwicklung ja, in Produktion nur wenn strikt auf interne Netzwerke beschränkt. Nie durch Fehlkonfiguration nach außen durchsickern lassen.
9Backoffice-API absichern?
Netzwerkseitig: nur aus internen Netzwerken/VPN. Applicationseitig: ROLE_BACKOFFICE, JWT mit kurzem Ablauf, Audit-Logging. Kein öffentlicher DNS-Eintrag.
10OAuth2 auch für Backoffice?
Nur wenn externe Systeme Zugriff brauchen. Für rein interne Systeme ist internes JWT einfacher zu implementieren und zu warten.