DTO-Leakage: Wenn die API bis ins Template tropft
DTO-Leakage passiert, wenn Datenstrukturen aus der API-Schicht direkt bis in die Komponenten durchgereicht werden.
Ohne Transformation.
Ohne Übersetzung.
Ohne Schutzschicht.
Das klingt pragmatisch.
Und ja: Für die ersten zwei Tickets fühlt es sich auch so an.
Die API liefert Daten. Die Komponente braucht Daten. Also nimmt die Komponente eben das DTO.
Was soll schon passieren?
Eine ganze Menge.

Was ist ein DTO?
Abschnitt betitelt „Was ist ein DTO?“Ein DTO ist ein Data Transfer Object.
Es beschreibt, wie Daten über eine technische Grenze transportiert werden. Zum Beispiel zwischen Backend und Frontend.
Ein DTO ist damit kein UI-Modell.
Es ist kein Domain-Modell.
Es ist kein ViewModel.
Es ist ein Transportmodell.
Das ist ein wichtiger Unterschied.
Ein DTO beantwortet die Frage:
Wie kommt die Information über die Leitung?
Ein ViewModel beantwortet dagegen die Frage:
Was braucht die UI, um ihren Zustand korrekt und verständlich darzustellen?
Wenn diese beiden Fragen mit derselben Datenstruktur beantwortet werden, entsteht Kopplung.
Wie DTO-Leakage aussieht
Abschnitt betitelt „Wie DTO-Leakage aussieht“Ein typisches Beispiel:
// API gibt zurück:interface TaskDto { task_id: string; created_at: string; // ISO-String assigned_to_user_id: string | null;}Und dieses DTO landet direkt in der Komponente:
@Component(...)export class TaskCardComponent { @Input() task: TaskDto; // ← DTO im UI-Layer}Vielleicht sogar noch im Template:
<article> <h3>{{ task.task_id }}</h3>
<p>Erstellt am: {{ task.created_at | date }}</p>
<p>Zugewiesen an: {{ task.assigned_to_user_id ?? 'Unzugewiesen' }}</p></article>Das sieht harmlos aus.
Aber hier sind bereits mehrere Grenzen verletzt:
- Die Komponente kennt API-Feldnamen.
- Das Template kennt technische Null-Semantik.
- Datumsformatierung hängt an einem Transportformat.
- UI-Text entsteht aus API-Struktur.
- Backend-Änderungen können UI-Code brechen.
Das ist DTO-Leakage.

Warum das am Anfang so verführerisch ist
Abschnitt betitelt „Warum das am Anfang so verführerisch ist“DTOs direkt zu verwenden fühlt sich effizient an.
Man spart sich ein Mapping. Man spart sich ein weiteres Interface. Man spart sich scheinbar Code.
Und manchmal ist das sogar okay.
Wenn du einen kleinen Prototypen baust, ein internes Wegwerf-Tool oder eine sehr dünne Oberfläche über einer stabilen API, kann direkte DTO-Nutzung vertretbar sein.
Das Problem beginnt, wenn daraus Architektur wird.
Dann ist das DTO nicht mehr nur ein Transportobjekt. Es wird plötzlich zur stillen Quelle der Wahrheit für die UI.
Und ab diesem Moment entscheidet die API-Struktur mit über dein Template.
Das ist der Punkt, an dem Pragmatismus langsam in Kopplung kippt.
Was passiert, wenn DTOs durchsickern?
Abschnitt betitelt „Was passiert, wenn DTOs durchsickern?“DTO-Leakage erzeugt selten sofort einen großen Fehler.
Es erzeugt viele kleine.
Die API ändert einen Feldnamen?
task_id -> idPlötzlich brechen Komponenten, Templates, Tests und vielleicht sogar Sortierlogik.
Das Backend-Team wechselt von snake_case zu camelCase?
created_at -> createdAtDann beginnt die große Suche durch Komponenten und Templates.
Ein ISO-String soll als Date behandelt werden?
created_at: string;Dann landet Formatierungs- oder Parsing-Logik irgendwo in der Komponente.
Ein null bedeutet fachlich „Unzugewiesen“?
assigned_to_user_id: null;Dann steht Business-Logik im Template.
Und irgendwann sieht man Code wie diesen:
@if (task.assigned_to_user_id) {<span>{{ getUserName(task.assigned_to_user_id) }}</span>} @else {<span>Unzugewiesen</span>}Das Problem ist nicht diese eine Zeile.
Das Problem ist, dass die UI anfängt, API-Semantik zu interpretieren.
Das eigentliche Problem: falsche Richtung der Abhängigkeit
Abschnitt betitelt „Das eigentliche Problem: falsche Richtung der Abhängigkeit“DTO-Leakage bedeutet: Die UI richtet sich nach der API.
Das klingt erstmal normal. Schließlich kommen die Daten ja von dort.
Architektonisch ist es aber gefährlich.
Die UI sollte nicht wissen müssen, ob das Backend task_id, taskId, id oder identifier liefert. Die UI braucht eine Aufgabe mit einer ID.
Die UI sollte nicht wissen müssen, ob ein Datum als ISO-String, Timestamp oder verschachteltes Objekt kommt. Die UI braucht ein darstellbares Datum.
Die UI sollte nicht wissen müssen, dass null fachlich „Unzugewiesen“ bedeutet. Die UI braucht einen Text, den sie anzeigen kann.
Die API gehört zur Infrastruktur. Die Komponente gehört zur Präsentation. Dazwischen braucht es Übersetzung.

Die Alternative: ViewModel
Abschnitt betitelt „Die Alternative: ViewModel“Die Komponente sollte kein TaskDto kennen.
Sie sollte ein Modell bekommen, das für die Darstellung gemacht ist.
interface TaskViewModel { id: string; createdAt: Date; assignee: string;}Das ViewModel beschreibt nicht, wie Daten transportiert werden.
Es beschreibt, was die UI braucht.
Die Übersetzung passiert an einer klaren Grenze:
function toTaskViewModel(dto: TaskDto): TaskViewModel { return { id: dto.task_id, createdAt: new Date(dto.created_at), assignee: dto.assigned_to_user_id ?? 'Unzugewiesen', };}Die Komponente kennt danach nur noch das ViewModel:
@Component(...)export class TaskCardComponent { @Input() task!: TaskViewModel;}Und das Template wird deutlich langweiliger:
<article> <h3>{{ task.id }}</h3>
<p>Erstellt am: {{ task.createdAt | date }}</p>
<p>Zugewiesen an: {{ task.assignee }}</p></article>Langweiliger Code ist oft guter Code.
Nicht, weil er banal ist. Sondern weil er an der richtigen Stelle keine Entscheidungen mehr treffen muss.
Der Mapper ist keine lästige Fleißarbeit
Abschnitt betitelt „Der Mapper ist keine lästige Fleißarbeit“Mapper werden oft als unnötiger Boilerplate empfunden.
Das stimmt manchmal.
Wenn ein Mapper nur Felder eins zu eins kopiert, fühlt er sich wie Ritual an.
function toViewModel(dto: TaskDto): TaskViewModel { return { id: dto.id, title: dto.title, };}Aber selbst dann erfüllt er eine wichtige Funktion:
Er markiert eine Grenze.
Heute kopiert er vielleicht nur. Morgen übersetzt er ein Datum. Übermorgen normalisiert er optionale Werte. Nächste Woche schützt er 15 Komponenten vor einer API-Änderung.
Ein Mapper ist nicht nur Code.
Er ist eine architektonische Sollbruchstelle.
Wenn sich die API ändert, soll genau dort etwas brechen. Nicht überall.

Was gehört in ein ViewModel?
Abschnitt betitelt „Was gehört in ein ViewModel?“Ein gutes ViewModel ist nicht einfach ein umbenanntes DTO.
Es ist auf die Darstellung zugeschnitten.
Typische Aufgaben eines ViewModels:
- technische Feldnamen in UI-Namen übersetzen
- Datumswerte in passende Typen oder Anzeigeformate bringen
nullundundefinedfachlich behandeln- IDs in lesbare Namen auflösen
- Anzeigezustände vorberechnen
- Berechtigungen für UI-Aktionen ableiten
- Loading- und Error-Zustände konsistent bereitstellen
- komplexe Bedingungen aus dem Template entfernen
Beispiel:
interface TaskViewModel { id: string; title: string; createdAtLabel: string; assigneeLabel: string; canEdit: boolean; isOverdue: boolean;}Dann muss das Template nicht mehr wissen, wie „überfällig“ berechnet wird.
@if (task.isOverdue) {<p class="warning">Überfällig</p>}Das Template konsumiert eine Entscheidung.
Es trifft sie nicht selbst.
Aber ist das nicht doppelte Arbeit?
Abschnitt betitelt „Aber ist das nicht doppelte Arbeit?“Manchmal ja.
Aber die Alternative ist oft nicht „keine Arbeit“.
Die Alternative ist nur: Die Arbeit verteilt sich.
Ein bisschen Mapping im Template. Ein bisschen Null-Handling in der Komponente. Ein bisschen Datumslogik in einer Pipe. Ein bisschen API-Wissen im Test. Ein bisschen Sonderfall in einer zweiten Komponente.
Das fühlt sich kurzfristig schneller an, weil niemand einen Mapper schreibt.
Langfristig ist es langsamer, weil niemand mehr weiß, wo die Übersetzung eigentlich stattfindet.
Explizites Mapping ist sichtbare Arbeit.
DTO-Leakage ist versteckte Arbeit.
Wann darf ein DTO direkt verwendet werden?
Abschnitt betitelt „Wann darf ein DTO direkt verwendet werden?“Nicht jede DTO-Nutzung ist automatisch ein Architekturverbrechen.
Es gibt Fälle, in denen direkte Verwendung okay sein kann:
- sehr kleine Prototypen
- Wegwerf-Code
- interne Admin-Tools mit extrem kurzer Lebensdauer
- generierte API-Clients, die nur in der Infrastruktur bleiben
- Views, die wirklich nur rohe API-Daten anzeigen
- sehr stabile APIs mit bewusst identischem UI-Kontrakt
Aber sobald fachliche Darstellung ins Spiel kommt, würde ich vorsichtig werden.
Eine gute Faustregel:
Wenn das Template API-Feldnamen, technische Null-Werte oder Transportformate kennt, ist die Grenze wahrscheinlich undicht.
Wo sollte das Mapping passieren?
Abschnitt betitelt „Wo sollte das Mapping passieren?“Nicht in der Komponente.
Die Komponente ist ein Konsument. Sie sollte nicht wissen müssen, wie aus task_id ein id wird.
Das Mapping gehört in eine Adapter-, Domain- oder Application-Schicht. Der genaue Name ist weniger wichtig als die Richtung.
Ein möglicher Aufbau:
Infrastructure API Client DTOs
Application / Domain Mapper ViewModel Facade / Store
Presentation Component TemplateOder als Fluss:
TaskDto -> toTaskViewModel(dto) -> TaskViewModel -> Component -> TemplateDie Komponente bekommt das Ergebnis der Übersetzung.
Nicht den Rohstoff.

Tests werden einfacher
Abschnitt betitelt „Tests werden einfacher“DTO-Leakage macht Tests oft unnötig breit.
Wenn Komponenten DTOs kennen, müssen Komponententests API-Strukturen nachbauen.
const task: TaskDto = { task_id: '123', created_at: '2026-06-19T10:00:00Z', assigned_to_user_id: null,};Der Test testet dann indirekt API-Semantik, Mapping, Null-Handling und UI-Verhalten gleichzeitig.
Mit ViewModels wird der Komponententest kleiner:
const task: TaskViewModel = { id: '123', createdAt: new Date('2026-06-19T10:00:00Z'), assignee: 'Unzugewiesen',};Und der Mapper bekommt eigene, fokussierte Tests:
describe('toTaskViewModel', () => { it('maps unassigned tasks', () => { expect( toTaskViewModel({ task_id: '123', created_at: '2026-06-19T10:00:00Z', assigned_to_user_id: null, }), ).toEqual({ id: '123', createdAt: new Date('2026-06-19T10:00:00Z'), assignee: 'Unzugewiesen', }); });});Das ist der eigentliche Gewinn:
- Komponenten testen Darstellung.
- Mapper testen Übersetzung.
- Infrastrukturtests testen API-Anbindung.
Jede Schicht wird für das getestet, wofür sie verantwortlich ist.
Der Vorteil
Abschnitt betitelt „Der Vorteil“Wenn die API sich ändert, ändert sich der Mapper.
Nicht die Komponente.
Nicht das Template.
Nicht jeder Test.
Nicht jede Stelle, die zufällig mal created_at gelesen hat.
Das ist keine akademische Reinheit.
Das ist Wartbarkeit.
DTOs dürfen existieren. Sie dürfen nur nicht überall wohnen.
DTO-Leakage ist gefährlich, weil es pragmatisch aussieht.
Man spart sich ein Interface. Man spart sich einen Mapper. Man spart sich eine Entscheidung.
Aber man bezahlt später mit Kopplung.
Gute Frontend-Architektur bedeutet nicht, möglichst viele Modelle zu erfinden. Sie bedeutet, Grenzen bewusst zu ziehen.
Ein DTO gehört an die technische Grenze.
Ein ViewModel gehört zur UI.
Und irgendwo dazwischen sitzt die Übersetzung.
Nicht, weil Architektur hübsch aussehen soll.
Sondern weil Änderungen sonst dahin laufen, wo sie am teuersten sind: in Komponenten, Templates und Tests.
Die kurze Regel lautet:
DTOs rein. ViewModels raus. Komponenten bleiben sauber.