{ }
GET
REST API · CSV · XML · Bulk · Content-Negotiation
CSV, XML und Bulk-Endpoints
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?

18 Min. Lesezeit Content-Negotiation · CSV-Streaming · Bulk-Fehlermodell · OpenAPI Symfony · PHP 8.4 · REST

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.

10. FAQ: CSV, XML und Bulk-Endpoints in REST-APIs

1Wann Content-Negotiation, wann eigene Endpunkte?
Content-Negotiation für inhaltlich gleiche Darstellungen. Eigene Endpunkte wenn Semantik oder Format sich wesentlich unterscheidet oder browserfreundliche URLs benötigt werden.
2Warum StreamedResponse für CSV?
Verhindert, dass alle Datensätze gleichzeitig im Speicher gehalten werden. Mit Batches und flush() bleibt der Speicherbedarf konstant – unabhängig von der Exportgröße.
3Was ist HTTP 207 Multi-Status?
Signalisiert teils erfolgreiche, teils fehlerhafte Bulk-Operation. Response-Body enthält pro Eintrag einen eigenen Statuscode. Für nicht-atomare Bulk-Operationen geeignet.
4Maximale Größe für Bulk-Requests?
100–500 Einträge für synchrone Verarbeitung. Größere Batches asynchron: 202 Accepted mit Job-ID, Status über GET /api/jobs/{id} abrufbar.
5CSV in OpenAPI dokumentieren?
Mehrere content-Typen im responses-Objekt: application/json und text/csv. Content-Disposition-Header dokumentieren. Accept-Header als Parameter definieren.
6Welches XML-Tool in PHP?
XMLWriter für Streaming-Feeds. DOMDocument für komplexe Dokumente mit XSD-Validierung. SimpleXML nur zum Lesen, nicht für Feeds mit Namespaces.
7Timeout bei großen Bulk-Importen verhindern?
Asynchrone Verarbeitung: 202 Accepted mit Job-ID sofort zurückgeben. Verarbeitung als Queue-Job. Client pollt GET /api/jobs/{id} bis zum Abschluss.
8Muss Bulk-Operation atomar sein?
Nein. Atomar: alle oder keine, 422 bei Fehler. Nicht-atomar: jeder Eintrag unabhängig, 207 mit Einzel-Statuscodes. Wahl nach fachlichen Anforderungen dokumentieren.
9X-Accel-Buffering: warum wichtig?
Deaktiviert nginx-Pufferung für den Request. Ohne diesen Header sammelt nginx alle Chunks und sendet erst am Ende – Streaming bringt dann keinen Vorteil.
10Bulk-Endpoints mit partiellen Fehlern testen?
Testbatch mit gemischten Einträgen: valide und invalide im selben Request. Prüfen: HTTP 207, succeeded/failed im Summary, korrekte Fehler-Indizes für fehlerhafte Einträge.