in REST-APIs sauber designen
Alternative Formate und Massenoperationen stellen REST-APIs vor echte Designprobleme: Wann Content-Negotiation, wann eigene Endpunkte? Wie werden tausende Datensätze performant gestreamt? Wie kommuniziert eine Bulk-Operation partielle Fehler klar genug, damit Clients sinnvoll reagieren können?
Inhaltsverzeichnis
- 1. Warum alternative Formate in REST-APIs ein Designproblem sind
- 2. Content-Negotiation: Accept-Header vs. eigene Endpunkte
- 3. CSV-Export: Streaming statt Speicher-Bombe
- 4. XML-Feeds: Namespaces, Schemas und Validierung
- 5. Bulk-Endpoints: Semantik, Idempotenz und Transaktionsgrenzen
- 6. Partielle Fehler in Bulk-Operationen richtig modellieren
- 7. CSV, XML und Bulk in OpenAPI dokumentieren
- 8. Designentscheidungen im Vergleich
- 9. Zusammenfassung
- 10. FAQ
1. Warum alternative Formate in REST-APIs ein Designproblem sind
REST-APIs liefern in der Regel JSON. Das reicht für Browser-Frontends, mobile Apps und Microservice-Kommunikation völlig aus. Sobald aber Buchhaltungssysteme, ERP-Integrationen oder Datenanalyse-Teams angebunden werden sollen, kommen sofort andere Anforderungen: CSV für Excel-Direktimport, XML für DATEV oder SAP-Schnittstellen, und Bulk-Operationen, die mehrere hundert Datensätze in einem einzigen Request verarbeiten. Diese Anforderungen lassen sich nicht einfach mit einem zusätzlichen Serializer lösen – sie stellen Fragen an API-Design, Fehlermodellierung, Performance und Dokumentation, die im JSON-only-Kontext gar nicht auftauchen.
Der häufigste Fehler: Alternative Formate werden als technisches Detail behandelt, das ein Entwickler am Freitagnachmittag "schnell noch dazubaut". Das Ergebnis sind Endpunkte mit /export?format=csv, die keine klaren Fehlercodes liefern, kein richtiges HTTP-Caching unterstützen, den Accept-Header ignorieren und in OpenAPI gar nicht auftauchen. Integratoren greifen auf Trial-and-Error zurück, Bugs werden erst in der Produktion entdeckt, und die API ist faktisch undokumentiert für alle Nicht-JSON-Formate. Dabei lassen sich alle drei Problemfelder – alternative Formate, Streaming und Bulk-Operationen – mit klar definierten Mustern sauber lösen.
2. Content-Negotiation: Accept-Header vs. eigene Endpunkte
HTTP bietet mit Content-Negotiation einen Standardmechanismus: Der Client sendet einen Accept-Header, der Server antwortet mit dem am besten passenden Format und setzt den Content-Type der Antwort. Das klingt elegant, hat in der Praxis aber Grenzen. Browser senden Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8 – was dazu führt, dass ein Browser-Aufruf einer JSON-API plötzlich XML zurückbekommt. Außerdem lässt sich Content-Negotiation mit Proxies und CDNs schwieriger konfigurieren, da der Vary: Accept-Header korrekt gesetzt und vom Cache interpretiert werden muss.
Die pragmatische Alternative sind eigene Endpunkte oder Pfadsegmente für alternative Formate: GET /api/orders/export.csv oder GET /api/orders/feed.xml. Diese Variante ist browserfreundlich, cachebar ohne Vary-Komplexität und klar in Logs erkennbar. Für Bulk-Operationen empfiehlt sich ein eigener Endpunkt POST /api/orders/bulk, weil Bulk-Semantik (partielle Fehler, Transaktionsgrenzen, asynchrone Verarbeitung) so grundlegend anders ist als normale CRUD-Operationen, dass ein gemeinsamer Endpunkt mehr Verwirrung stiftet als er löst. Die Entscheidungsregel: Content-Negotiation für inhaltlich identische Darstellungen, eigene Endpunkte sobald Format oder Semantik sich wesentlich unterscheiden.
# OpenAPI: Content-Negotiation für JSON und CSV am selben Endpunkt
/api/orders:
get:
summary: Bestellungen abrufen
parameters:
- name: Accept
in: header
schema:
type: string
enum:
- application/json
- text/csv
default: application/json
responses:
'200':
description: Bestellungen
content:
application/json:
schema:
$ref: '#/components/schemas/OrderCollection'
text/csv:
schema:
type: string
format: binary
headers:
Content-Disposition:
schema:
type: string
example: 'attachment; filename="orders-2026-05-10.csv"'
'406':
description: Angefordertes Format wird nicht unterstützt
content:
application/json:
schema:
$ref: '#/components/schemas/ProblemDetail'
3. CSV-Export: Streaming statt Speicher-Bombe
Ein CSV-Export über eine REST-API beginnt immer mit derselben Versuchung: alle Datensätze aus der Datenbank laden, in ein String-Array umwandeln, als text/csv-Response zurückgeben. Bei 500 Zeilen funktioniert das. Bei 50.000 Zeilen hängt der PHP-Prozess mit 512 MB RAM im Speicher, während der Client wartet. Das Muster dahinter ist strukturell falsch: Die Response wird komplett im Speicher aufgebaut, bevor das erste Byte an den Client geht.
Die richtige Lösung ist HTTP-Streaming mit einer PHP-Generator-Funktion oder einem Symfony-StreamedResponse. Der Server öffnet die HTTP-Verbindung, sendet den CSV-Header, und schreibt dann Zeile für Zeile in den Output-Buffer – während er gleichzeitig in Batches aus der Datenbank liest. Der Client empfängt Daten kontinuierlich, und der Server benötigt nur so viel Speicher wie ein einziger Batch. Kritisch: Transfer-Encoding: chunked wird von PHP mit flush() aktiviert, und Symfony-Proxies oder nginx-Puffer müssen deaktiviert sein (X-Accel-Buffering: no), damit der Client Chunks sofort empfängt und nicht auf den gesamten Response wartet.
<?php
// Symfony StreamedResponse für großen CSV-Export ohne Speicher-Overhead
use Symfony\Component\HttpFoundation\StreamedResponse;
use Doctrine\ORM\EntityManagerInterface;
final class OrderExportController
{
public function __construct(
private readonly EntityManagerInterface $em,
) {}
public function __invoke(): StreamedResponse
{
return new StreamedResponse(function (): void {
$handle = fopen('php://output', 'w');
// UTF-8 BOM for Excel compatibility
fwrite($handle, "\xEF\xBB\xBF");
// Header row
fputcsv($handle, ['ID', 'Datum', 'Kunde', 'Betrag', 'Status'], ';');
$batchSize = 500;
$offset = 0;
do {
$rows = $this->em->createQuery(
'SELECT o.id, o.createdAt, o.customerName, o.total, o.status
FROM App\Entity\Order o ORDER BY o.id ASC'
)
->setFirstResult($offset)
->setMaxResults($batchSize)
->getArrayResult();
foreach ($rows as $row) {
fputcsv($handle, [
$row['id'],
$row['createdAt']->format('Y-m-d'),
$row['customerName'],
number_format($row['total'], 2, ',', '.'),
$row['status'],
], ';');
}
flush(); // Send chunk to client immediately
$this->em->clear(); // Free Doctrine identity map
$offset += $batchSize;
} while (count($rows) === $batchSize);
fclose($handle);
}, 200, [
'Content-Type' => 'text/csv; charset=UTF-8',
'Content-Disposition' => 'attachment; filename="orders-' . date('Y-m-d') . '.csv"',
'X-Accel-Buffering' => 'no', // Disable nginx buffering
'Cache-Control' => 'no-store',
]);
}
}
4. XML-Feeds: Namespaces, Schemas und Validierung
XML-Feeds für externe Systeme – ERP, DATEV, Google Merchant Center oder Vergleichsportale – folgen in der Regel einem vorgegebenen Schema, das der Empfänger erwartet. Das eigentliche Problem ist nicht die XML-Erzeugung, sondern die Konsistenz: Eine Abweichung im Namespace, ein fehlendes Pflichtfeld oder ein falsches Datumsformat führt dazu, dass der Empfänger den gesamten Feed ablehnt, ohne eine sinnvolle Fehlermeldung zu liefern. Valide XML nach einem definierten XSD-Schema zu produzieren ist deshalb kein optionaler Qualitätsschritt, sondern Grundbedingung für funktionierende Integrationen.
In PHP erzeugt man XML-Feeds am saubersten mit XMLWriter für Streaming oder mit DOMDocument für kleinere, strukturell komplexe Dokumente. SimpleXML ist für Lesen geeignet, aber nicht für sauberes Schreiben mit Namespaces. Nach der Erzeugung validiert man gegen das XSD-Schema mit DOMDocument::schemaValidate(). Im Produktionsbetrieb gehört diese Validierung in einen automatisierten Test, nicht in den Request-Handler selbst – denn Validierung kostet Zeit und schlägt bei Schema-Änderungen fehl, die man frühzeitig im CI erkennen will. Für den API-Endpunkt ist die Anforderung: valides XML ausliefern und einen Last-Modified-Header setzen, damit Clients effizient pollen können, ohne den Feed bei jeder Anfrage vollständig herunterzuladen.
5. Bulk-Endpoints: Semantik, Idempotenz und Transaktionsgrenzen
Ein Bulk-Endpoint nimmt mehrere Ressourcen in einem einzigen Request entgegen. Das klingt einfach, aber die Designfragen sind erheblich: Soll die Bulk-Operation atomar sein – entweder alle Einträge werden verarbeitet oder keiner? Oder soll jeder Eintrag unabhängig verarbeitet werden, mit individuellem Erfolg oder Fehler? Ist die Operation idempotent, also kann man sie sicher wiederholen, wenn die Antwort verloren gegangen ist? Diese Fragen bestimmen die HTTP-Methode, den Statuscode und das Fehlermodell.
Für atomare Bulk-Operationen (alle oder keine) eignet sich POST /api/orders/bulk mit einer Datenbanktransaction im Handler. Bei Fehler wird 422 oder 400 zurückgegeben, es wurde nichts persistiert. Für nicht-atomare Bulk-Operationen mit individuellen Ergebnissen gibt der Endpunkt 207 Multi-Status zurück – jeder Eintrag im Request hat einen eigenen Statuscode in der Antwort. Idempotente Bulk-Operationen – etwa das Setzen des Status für eine Menge von Bestellungen – passen zu PATCH /api/orders/bulk mit einer eindeutigen Idempotency-Key im Header. Der Server prüft den Key und liefert bei doppeltem Request die gecachte Antwort, ohne die Operation erneut auszuführen.
// POST /api/orders/bulk — Nicht-atomare Bulk-Erstellung, 207 Multi-Status Response
{
"results": [
{
"index": 0,
"status": 201,
"id": "ORD-10042",
"href": "/api/orders/ORD-10042"
},
{
"index": 1,
"status": 422,
"error": {
"type": "https://mironsoft.de/errors/validation-failed",
"title": "Validierungsfehler",
"detail": "Feld 'customerEmail' fehlt oder ist ungültig.",
"field": "customerEmail"
}
},
{
"index": 2,
"status": 201,
"id": "ORD-10043",
"href": "/api/orders/ORD-10043"
}
],
"summary": {
"total": 3,
"succeeded": 2,
"failed": 1
}
}
6. Partielle Fehler in Bulk-Operationen richtig modellieren
HTTP-Statuscodes sind für einzelne Ressourcen-Requests ausgelegt: 200, 201, 400, 404, 422. Sobald eine Bulk-Operation teils erfolgreich und teils fehlerhaft abläuft, reicht kein einzelner Statuscode mehr aus. Das HTTP-Protokoll definiert mit 207 Multi-Status genau für diesen Fall einen eigenen Statuscode, der aus dem WebDAV-Standard stammt, aber für jede Art von Bulk-Operation in REST-APIs anwendbar ist. Der Response-Body enthält pro Eingang einen eigenen Statuscode, sodass der Client präzise weiß, welche Einträge erfolgreich verarbeitet wurden und welche nicht.
Neben 207 Multi-Status gibt es zwei weitere verbreitete Muster für partielle Fehler. Das erste: Der Endpunkt akzeptiert atomare Semantik und gibt bei jedem Fehler 422 zurück, mit einer Liste aller Validierungsfehler im Body. Das zweite: Die Bulk-Operation wird asynchron verarbeitet, der Endpunkt gibt sofort 202 Accepted mit einer Job-ID zurück, und der Client pollt GET /api/jobs/{id} für den Status. Welches Muster passt, hängt von der Verarbeitungsdauer und den Anforderungen des Clients ab: Synchrone 207-Antworten sind für kleine Batches (bis ~100 Einträge) geeignet, asynchrone Jobs für große Importe (tausende Einträge), bei denen eine synchrone Verbindung timeout-gefährdet wäre.
7. CSV, XML und Bulk in OpenAPI dokumentieren
Die größte Dokumentationslücke in REST-APIs mit alternativen Formaten ist OpenAPI. Viele Teams dokumentieren JSON sauber, aber CSV-Exports fehlen im Schema komplett, Bulk-Endpunkte haben keine Beispiele für 207-Responses, und XML-Feeds tauchen nicht einmal als Endpunkt auf. Integratoren finden die Formate nur durch Ausprobieren oder Nachfragen – was in der Praxis zu langen Onboarding-Zeiten und Support-Aufwand führt.
OpenAPI 3.1 unterstützt mehrere content-Typen pro Response, sodass JSON und CSV am selben Endpunkt dokumentiert werden können. Für Bulk-Endpoints mit 207 Multi-Status definiert man ein Schema für das Array von Einzel-Ergebnissen, das sowohl erfolgreiche als auch fehlerhafte Einträge modelliert. Für XML-Feeds empfiehlt sich das Hinterlegen eines Beispiel-Dokuments in der example-Property. Der wichtigste Dokumentationsschritt für Bulk-Operationen: das summary-Objekt in der Response explizit modellieren, damit Integratoren verstehen, dass partielle Fehler nicht mit einem HTTP-Fehlercode signalisiert werden, sondern im Body zu finden sind.
# OpenAPI: Bulk-Endpoint mit 207 Multi-Status
/api/orders/bulk:
post:
summary: Mehrere Bestellungen in einem Request anlegen
description: >
Nicht-atomare Bulk-Erstellung. Jede Bestellung wird unabhängig verarbeitet.
Partielle Fehler werden mit HTTP 207 Multi-Status und individuellen Statuscodes
pro Eintrag zurückgemeldet.
requestBody:
required: true
content:
application/json:
schema:
type: object
required: [orders]
properties:
orders:
type: array
minItems: 1
maxItems: 500
items:
$ref: '#/components/schemas/OrderCreate'
example:
orders:
- customerEmail: "max@example.com"
total: 149.99
- customerEmail: ""
total: 89.00
responses:
'207':
description: Bulk-Ergebnis mit individuellen Statuscodes
content:
application/json:
schema:
$ref: '#/components/schemas/BulkResult'
'400':
description: Ungültiger Request (z.B. Array fehlt oder zu groß)
'429':
description: Rate-Limit für Bulk-Operationen überschritten
8. Designentscheidungen im Vergleich
Die Wahl zwischen verschiedenen Ansätzen für alternative Formate und Bulk-Operationen ist keine Stilfrage, sondern hat direkte Konsequenzen für Cachability, Clientkomplexität und Fehlerdiagnose.
| Szenario | Problematischer Ansatz | Empfohlenes Muster | Vorteil |
|---|---|---|---|
| CSV-Export | ?format=csv Query-Parameter |
Accept-Header oder /export.csv |
HTTP-Standard, cachebar mit Vary |
| Großer Export | Alles in Speicher laden | StreamedResponse + Batches | Konstanter Speicherbedarf |
| Bulk-Fehler | 500 oder 400 bei Teilfehler | 207 Multi-Status + Index | Präzise Fehlerlokalisation |
| Großer Bulk-Import | Synchroner Request (Timeout) | 202 Accepted + Job-Polling | Keine Timeout-Gefahr |
| XML-Validierung | Nur manuelle Prüfung | XSD-Validierung im CI | Schema-Fehler früh erkennen |
Ein häufig übersehener Punkt bei Bulk-Endpoints: Die maximale Batch-Größe muss explizit im OpenAPI-Schema und im Response-Header X-Max-Bulk-Size kommuniziert werden. Clients, die keine Größenbegrenzung kennen, schicken irgendwann Batches mit 10.000 Einträgen, die den Server blockieren. Die 429-Antwort sollte einen Retry-After-Header enthalten und explizit erklären, ob das Limit pro Minute, Stunde oder Verbindung gilt.
9. Zusammenfassung
CSV, XML und Bulk-Endpoints sind keine Sonderfälle, die man nachträglich in eine REST-API hineinklebt – sie sind vollwertige Teile des API-Designs mit eigenen Anforderungen an HTTP-Semantik, Fehlermodellierung und Dokumentation. Content-Negotiation über den Accept-Header ist der HTTP-Standard für alternative Formate am selben Endpunkt; eigene Pfade wie /export.csv sind die pragmatische Alternative für browserfreundliche, klar loggbare Exporte. Streaming mit StreamedResponse und Datenbank-Batches ist bei großen Exporten nicht optional, sondern Grundbedingung für stabilen Betrieb.
Bulk-Endpoints brauchen klare Transaktionsgrenzen: atomar mit 422 bei Fehler oder nicht-atomar mit 207 Multi-Status und individuellen Ergebnissen pro Eintrag. Asynchrone Verarbeitung mit 202 und Job-Polling ist die richtige Wahl für große Importe. OpenAPI-Dokumentation muss alle Formate, alle Statuscodes und die Fehlerstruktur abdecken – auch für Nicht-JSON-Formate. Teams, die alternative Formate und Bulk-Operationen von Anfang an in ihr API-Design einplanen, vermeiden den typischen Nachbesserungsaufwand und liefern ihren Integratoren eine API, die ohne Trial-and-Error funktioniert.
CSV, XML und Bulk in REST-APIs — Das Wichtigste auf einen Blick
Content-Negotiation
Accept-Header für inhaltlich gleiche Formate. Eigene Endpunkte sobald Semantik oder Format sich wesentlich unterscheidet. Immer Vary: Accept setzen.
CSV-Streaming
StreamedResponse + Datenbank-Batches + flush(). X-Accel-Buffering: no für nginx. UTF-8 BOM für Excel. Kein Gesamtergebnis im Speicher aufbauen.
Bulk-Fehlermodell
207 Multi-Status für partielle Fehler. Index in der Antwort für Zuordnung. Summary-Objekt mit total/succeeded/failed. Maximale Batch-Größe dokumentieren.
OpenAPI-Dokumentation
Alle Formate als content-Typen. 207-Schema vollständig modellieren. XML-Beispiel hinterlegen. Rate-Limits und Batch-Größen als Parameter dokumentieren.