Zum Inhalt springen

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.

DTO Leakage.

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.

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.

DTO im UI-Layer

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.

DTO-Leakage erzeugt selten sofort einen großen Fehler.

Es erzeugt viele kleine.

Die API ändert einen Feldnamen?

task_id -> id

Plötzlich brechen Komponenten, Templates, Tests und vielleicht sogar Sortierlogik.

Das Backend-Team wechselt von snake_case zu camelCase?

created_at -> createdAt

Dann 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.

Falsche Richtung vs Richtige Richtung

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.

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.

Mapper als Brandschutzwand

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
  • null und undefined fachlich 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.

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.

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.

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
Template

Oder als Fluss:

TaskDto
-> toTaskViewModel(dto)
-> TaskViewModel
-> Component
-> Template

Die Komponente bekommt das Ergebnis der Übersetzung.

Nicht den Rohstoff.

Layer-Fluss: Infrastructure/API DTO → Mapper/Adapter → Feature Store/Facade → ViewModel → Component/Template. Markiere: DTO endet vor Presentation.

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.

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.