API-Wrapper für Legacy-Systeme Legacy-Integration

API-Wrapper für Legacy-Systeme bauen: Modernisierung ohne Risiko

Carola Schulte
Carola Schulte 1. Januar 2026 15 min Lesezeit

Das Legacy-System läuft. Seit 15 Jahren. Keiner traut sich ran. Aber plötzlich soll es mit der neuen App reden, Daten an den externen Partner liefern, oder ins moderne Ökosystem integriert werden. Die Lösung: Ein API-Wrapper, der das alte System kapselt, ohne es anzufassen. Klingt einfach – hat aber Tücken.

Kurz gesagt: Ein API-Wrapper stellt eine moderne REST- oder GraphQL-Schnittstelle vor ein Legacy-System. Das Altsystem bleibt unverändert, während neue Clients über eine saubere API zugreifen. Der Wrapper übersetzt zwischen alter und neuer Welt.

Wann ist ein API-Wrapper die richtige Wahl?

Nicht jedes Legacy-System braucht einen Wrapper. Manchmal ist ein Rewrite sinnvoller, manchmal reicht eine direkte Datenbankanbindung. Ein Wrapper macht Sinn, wenn:

  • Das System stabil läuft – und niemand das Risiko eingehen will, es anzufassen
  • Mehrere Clients zugreifen sollen – Mobile App, Partner-API, internes Dashboard
  • Die Geschäftslogik komplex ist – und im Legacy-Code steckt, den niemand neu schreiben will
  • Schrittweise Migration geplant ist – der Wrapper wird zum Anti-Corruption Layer
  • Dokumentation fehlt – der Wrapper erzwingt eine definierte Schnittstelle
Wann nicht: Wenn das Legacy-System instabil ist, keine klaren Schnittstellen hat, oder die Performance bereits am Limit läuft. Ein Wrapper löst keine grundlegenden Architekturprobleme – er versteckt sie nur.

Architektur-Patterns

1. Direkter Datenbank-Wrapper

Der einfachste Ansatz: Der Wrapper greift direkt auf die Legacy-Datenbank zu und stellt die Daten über eine API bereit.

// PHP-Beispiel: Direkter DB-Wrapper
class LegacyCustomerApi {
    private PDO $legacyDb;

    public function __construct(PDO $legacyDb) {
        $this->legacyDb = $legacyDb;
    }

    public function getCustomer(int $id): array {
        // Legacy-Tabelle hat kryptische Spaltennamen
        $stmt = $this->legacyDb->prepare("
            SELECT KDNR as id,
                   KDNAME as name,
                   KDORT as city,
                   KDPLZ as zip,
                   KDSTR as street,
                   KDUMSATZ as revenue
            FROM STAMM_KD
            WHERE KDNR = ?
        ");
        $stmt->execute([$id]);
        $row = $stmt->fetch(PDO::FETCH_ASSOC);

        if (!$row) {
            throw new NotFoundException("Customer not found");
        }

        // Transformation in modernes Format
        return [
            'id' => (int) $row['id'],
            'name' => trim($row['name']),
            'address' => [
                'street' => trim($row['street']),
                'zip' => trim($row['zip']),
                'city' => trim($row['city'])
            ],
            'revenue' => (float) $row['revenue']
        ];
    }
}
Vorteile: Schnell implementiert, keine Änderung am Legacy-System nötig, volle Kontrolle über das Datenformat.
Risiken: Sie umgehen die Geschäftslogik des Legacy-Systems. Validierungen, Trigger, Stored Procedures – alles wird ignoriert. Wenn das Legacy-System Daten anders interpretiert als Sie denken, haben Sie ein Problem.

Klare Linie: Direkter DB-Zugriff ist nur vertretbar für read-only Szenarien oder Reporting – niemals für schreibende Geschäftsprozesse.

2. Service-Wrapper (über bestehende Schnittstellen)

Besser, wenn das Legacy-System bereits Schnittstellen hat – auch wenn sie alt sind (SOAP, XML-RPC, proprietäre Protokolle):

// Wrapper um einen SOAP-Service
class LegacyOrderWrapper {
    private SoapClient $soapClient;

    public function __construct(string $wsdlUrl) {
        $this->soapClient = new SoapClient($wsdlUrl, [
            'trace' => true,
            'exceptions' => true,
            'connection_timeout' => 30
        ]);
    }

    public function createOrder(array $orderData): array {
        // Transformation: modernes JSON -> Legacy SOAP
        $legacyRequest = $this->transformToLegacyFormat($orderData);

        try {
            $response = $this->soapClient->BestellungAnlegen($legacyRequest);

            // Transformation: Legacy Response -> modernes JSON
            return $this->transformToModernFormat($response);

        } catch (SoapFault $e) {
            // Legacy-Fehlercodes in HTTP-Status übersetzen
            throw $this->mapLegacyError($e);
        }
    }

    private function transformToLegacyFormat(array $data): object {
        return (object) [
            'BESTELLKOPF' => (object) [
                'KDNR' => $data['customerId'],
                'BESTDAT' => date('Ymd'),
                'LIEFART' => $this->mapDeliveryType($data['delivery'])
            ],
            'BESTELLPOS' => array_map(fn($item) => (object) [
                'ARTNR' => $item['sku'],
                'MENGE' => $item['quantity'],
                'EPREIS' => $item['price']
            ], $data['items'])
        ];
    }
}

Dieser Ansatz respektiert die Geschäftslogik des Legacy-Systems – Sie rufen dieselben Operationen auf, die auch die Legacy-UI nutzt.

3. Screen Scraping (wenn nichts anderes geht)

Manchmal gibt es keine API, keinen Datenbankzugang, nur eine alte Web-Oberfläche. Dann bleibt Screen Scraping:

// Screen Scraping als letzte Option
class LegacyScreenWrapper {
    private HttpClient $client;
    private string $sessionCookie;

    public function login(string $user, string $pass): void {
        $response = $this->client->post('/login.asp', [
            'form_params' => [
                'USER' => $user,
                'PASS' => $pass
            ]
        ]);

        // Session-Cookie extrahieren
        $this->sessionCookie = $this->extractSessionCookie($response);
    }

    public function getInventory(string $productId): array {
        $response = $this->client->get('/inventory.asp', [
            'query' => ['ARTNR' => $productId],
            'headers' => ['Cookie' => $this->sessionCookie]
        ]);

        $html = $response->getBody()->getContents();

        // HTML parsen - fragil, aber manchmal alternativlos
        preg_match('/Bestand:\s*(\d+)/', $html, $matches);
        $stock = (int) ($matches[1] ?? 0);

        preg_match('/Lagerort:\s*([A-Z0-9-]+)/', $html, $matches);
        $location = $matches[1] ?? 'UNKNOWN';

        return [
            'productId' => $productId,
            'stock' => $stock,
            'location' => $location
        ];
    }
}
Warnung: Screen Scraping ist extrem fragil. Jede Änderung an der Legacy-UI kann den Wrapper brechen. Nutzen Sie es nur, wenn wirklich keine Alternative existiert – und investieren Sie in umfangreiche Tests und Monitoring.

Der Anti-Corruption Layer

Ein API-Wrapper ist mehr als nur Protokoll-Übersetzung. Er sollte als Anti-Corruption Layer (ACL) fungieren – eine Schutzschicht, die verhindert, dass Legacy-Konzepte in Ihre moderne Architektur durchsickern.

Domain-Übersetzung

Das Legacy-System hat andere Begriffe, andere Strukturen, andere Annahmen:

class CustomerMapper {
    // Legacy kennt nur "KDTYP" mit Werten 1, 2, 3
    // Modern kennt "customerType" mit business, private, partner

    private const TYPE_MAP = [
        1 => 'private',
        2 => 'business',
        3 => 'partner'
    ];

    public function toModern(array $legacyCustomer): Customer {
        return new Customer(
            id: new CustomerId($legacyCustomer['KDNR']),
            name: $this->parseName($legacyCustomer['KDNAME']),
            type: CustomerType::from(self::TYPE_MAP[$legacyCustomer['KDTYP']] ?? 'private'),
            address: $this->parseAddress($legacyCustomer),
            // Legacy speichert Umsatz in Pfennig (ja, wirklich)
            revenue: Money::fromCents($legacyCustomer['KDUMSATZ'])
        );
    }

    private function parseName(string $legacyName): CustomerName {
        // Legacy: "Müller, Hans" oder "Firma XYZ GmbH"
        if (str_contains($legacyName, ',')) {
            [$lastName, $firstName] = explode(',', $legacyName, 2);
            return CustomerName::person(trim($firstName), trim($lastName));
        }
        return CustomerName::company($legacyName);
    }
}

Fehler-Übersetzung

Legacy-Systeme haben oft kryptische Fehlercodes. Der Wrapper übersetzt sie in verständliche HTTP-Responses:

class LegacyErrorMapper {
    private const ERROR_MAP = [
        'E001' => ['status' => 404, 'message' => 'Customer not found'],
        'E002' => ['status' => 400, 'message' => 'Invalid customer number format'],
        'E017' => ['status' => 409, 'message' => 'Customer has open orders, cannot delete'],
        'E099' => ['status' => 503, 'message' => 'Legacy system temporarily unavailable'],
        // Die Klassiker:
        'FEHLER IM SYSTEM' => ['status' => 500, 'message' => 'Internal server error'],
        'ZUGRIFF VERWEIGERT' => ['status' => 403, 'message' => 'Access denied'],
    ];

    public function map(string $legacyError): ApiException {
        $mapped = self::ERROR_MAP[$legacyError] ?? [
            'status' => 500,
            'message' => 'Unknown legacy error: ' . $legacyError
        ];

        return new ApiException(
            $mapped['message'],
            $mapped['status'],
            ['legacyCode' => $legacyError]
        );
    }
}

Performance-Optimierung

Legacy-Systeme sind selten performant. Der Wrapper kann das teilweise kompensieren:

Caching

class CachedLegacyWrapper {
    private LegacyApi $legacy;
    private CacheInterface $cache;

    public function getProduct(string $sku): array {
        $cacheKey = "product:{$sku}";

        return $this->cache->get($cacheKey, function() use ($sku) {
            // Legacy-Call nur wenn nicht im Cache
            $product = $this->legacy->getProduct($sku);

            // TTL abhängig von Datentyp
            // Produktstammdaten: länger cachen
            // Bestandsdaten: kürzer oder gar nicht
            return $product;
        }, ttl: 3600);
    }

    public function getStock(string $sku): array {
        // Bestand nicht cachen - muss live sein
        return $this->legacy->getStock($sku);
    }
}

Request Batching

class BatchingWrapper {
    private array $pendingRequests = [];
    private LegacyApi $legacy;

    public function getCustomer(int $id): Promise {
        // Request sammeln statt sofort ausführen
        $deferred = new Deferred();
        $this->pendingRequests[] = [
            'type' => 'customer',
            'id' => $id,
            'deferred' => $deferred
        ];

        return $deferred->promise();
    }

    public function flush(): void {
        if (empty($this->pendingRequests)) {
            return;
        }

        // Alle Customer-IDs sammeln
        $customerIds = array_column(
            array_filter($this->pendingRequests, fn($r) => $r['type'] === 'customer'),
            'id'
        );

        // Ein Batch-Call statt vieler Einzelcalls
        $customers = $this->legacy->getCustomersBatch($customerIds);

        // Ergebnisse verteilen
        foreach ($this->pendingRequests as $request) {
            if ($request['type'] === 'customer') {
                $request['deferred']->resolve($customers[$request['id']] ?? null);
            }
        }

        $this->pendingRequests = [];
    }
}

Sicherheits-Aspekte

Der Wrapper ist eine Angriffsfläche. Er muss sicherer sein als das Legacy-System dahinter:

Input-Validierung

class SecureWrapper {
    public function getCustomer(int $id): array {
        // Validierung VOR dem Legacy-Call
        if ($id <= 0 || $id > 999999999) {
            throw new ValidationException('Invalid customer ID');
        }

        return $this->legacy->getCustomer($id);
    }

    public function searchCustomers(string $query): array {
        // SQL Injection verhindern, auch wenn Legacy anfällig wäre
        $sanitized = preg_replace('/[^a-zA-Z0-9äöüÄÖÜß\s-]/', '', $query);

        if (strlen($sanitized) < 2) {
            throw new ValidationException('Search query too short');
        }

        return $this->legacy->searchCustomers($sanitized);
    }
}

Rate Limiting

class RateLimitedWrapper {
    private RateLimiter $limiter;

    public function createOrder(array $data): array {
        $clientId = $this->getClientId();

        // Legacy-System schützen vor Überlastung
        if (!$this->limiter->allow($clientId, 'orders', limit: 100, window: 3600)) {
            throw new TooManyRequestsException(
                'Order limit exceeded. Max 100 orders per hour.'
            );
        }

        return $this->legacy->createOrder($data);
    }
}

Audit Logging

class AuditWrapper {
    public function updateCustomer(int $id, array $data): array {
        $before = $this->legacy->getCustomer($id);

        $result = $this->legacy->updateCustomer($id, $data);

        $this->logger->info('Customer updated via API', [
            'customerId' => $id,
            'clientId' => $this->getClientId(),
            'changes' => $this->diff($before, $result),
            'timestamp' => time(),
            'ip' => $this->getClientIp()
        ]);

        return $result;
    }
}

Testing-Strategien

Contract Tests

Der Wrapper muss stabil bleiben, auch wenn sich das Legacy-System ändert:

class LegacyContractTest extends TestCase {
    /**
     * Dieser Test schlägt fehl, wenn das Legacy-System
     * sein Antwortformat ändert
     */
    public function testCustomerResponseFormat(): void {
        $response = $this->legacyApi->getCustomer(12345);

        // Pflichtfelder müssen vorhanden sein
        $this->assertArrayHasKey('KDNR', $response);
        $this->assertArrayHasKey('KDNAME', $response);
        $this->assertArrayHasKey('KDTYP', $response);

        // Datentypen müssen stimmen
        $this->assertIsNumeric($response['KDNR']);
        $this->assertIsString($response['KDNAME']);
        $this->assertContains($response['KDTYP'], [1, 2, 3]);
    }
}

Integration Tests gegen Testdaten

class WrapperIntegrationTest extends TestCase {
    /**
     * Test gegen bekannte Testdaten im Legacy-System
     */
    public function testGetKnownCustomer(): void {
        // Testkunde existiert im Legacy-System
        $result = $this->wrapper->getCustomer(99999);

        $this->assertEquals('Test GmbH', $result['name']);
        $this->assertEquals('business', $result['type']);
        $this->assertEquals('12345', $result['address']['zip']);
    }
}

Monitoring & Alerting

Ein Wrapper ohne Monitoring ist eine Zeitbombe:

class MonitoredWrapper {
    private MetricsCollector $metrics;

    public function getCustomer(int $id): array {
        $start = microtime(true);

        try {
            $result = $this->legacy->getCustomer($id);

            $this->metrics->timing('legacy.customer.get', microtime(true) - $start);
            $this->metrics->increment('legacy.customer.get.success');

            return $result;

        } catch (LegacyException $e) {
            $this->metrics->increment('legacy.customer.get.error', [
                'error_code' => $e->getCode()
            ]);

            // Alert bei kritischen Fehlern
            if ($e->getCode() === 'E099') {
                $this->alerting->critical('Legacy system unavailable');
            }

            throw $e;
        }
    }
}

Wichtige Metriken:

  • Response Time: Wird das Legacy-System langsamer?
  • Error Rate: Steigen die Fehler?
  • Request Volume: Überlastet der Wrapper das Legacy-System?
  • Cache Hit Rate: Funktioniert das Caching?

Typische Fallstricke

1. Leaky Abstractions

Der häufigste Fehler: Legacy-Konzepte sickern durch den Wrapper durch.

Schlecht:

// Legacy-Feldnamen in der API
{
  "KDNR": 12345,
  "KDNAME": "Müller, Hans",
  "KDTYP": 1
}

Besser:

// Moderne, selbsterklärende Struktur
{
  "id": 12345,
  "name": "Hans Müller",
  "type": "private"
}

2. Wrapper-Bypass

Entwickler umgehen den Wrapper und greifen direkt auf die Legacy-Datenbank zu – „nur für diesen einen Report". Sechs Monate später gibt es zehn solcher Direktzugriffe, und der Wrapper ist wertlos.

Lösung: Netzwerk-Segmentierung. Die Legacy-Datenbank ist nur vom Wrapper-Service erreichbar.

3. Scope Creep

Der Wrapper wächst und wächst. Plötzlich enthält er Geschäftslogik, die eigentlich ins Legacy-System oder einen neuen Service gehört.

Lösung: Klare Regel: Der Wrapper übersetzt nur. Keine fachliche Geschäftslogik – nur technische Schutz- und Übersetzungslogik (Error Mapping, Rate Limiting, Input Validation).

4. Wartungsaufwand unterschätzen

Der Wrapper muss gepflegt werden. Wenn das Legacy-System Updates bekommt, muss der Wrapper angepasst werden. Wenn neue Felder hinzukommen, muss der Wrapper erweitert werden.

Lösung: Wrapper-Maintenance als festen Posten einplanen. Nicht als „macht man nebenbei".

Fazit

Ein API-Wrapper ist kein Allheilmittel, aber oft der pragmatischste Weg, Legacy-Systeme zu integrieren. Der Schlüssel liegt in:

  • Klare Trennung: Der Wrapper übersetzt, nicht mehr
  • Saubere Abstraktion: Keine Legacy-Konzepte nach außen durchlassen
  • Robustheit: Caching, Rate Limiting, Error Handling
  • Monitoring: Probleme erkennen, bevor User sie melden
  • Dokumentation: Die API ist die neue Wahrheit

Zeitliche Perspektive: Ein Wrapper ist fast immer ein temporärer Baustein – mit einer Lebensdauer von Jahren, nicht Wochen. Planen Sie entsprechend.

Der wichtigste Punkt: Ein guter Wrapper macht das Legacy-System nicht besser – aber er macht es nutzbar. Und das ist oft genug, um Jahre zu gewinnen, bis ein echter Ersatz möglich ist.

Carola Schulte

Carola Schulte

Software-Architektin mit 25+ Jahren Erfahrung in Legacy-Modernisierung, Security-Audits und System-Design.

Beratung anfragen

Legacy-System integrieren?

Ich analysiere Ihre bestehende Systemlandschaft und entwickle eine Integrationsstrategie – pragmatisch, sicher, wartbar.

Kostenlose Erstanalyse anfragen