DTO leakage: when the API drips into the template
DTO leakage happens when data structures from the API layer get passed straight through into components.
No transformation. No translation. No protective layer.
That sounds pragmatic.
And yes: for the first two tickets, it feels that way too.
The API delivers data. The component needs data. So the component just takes the DTO.
What could possibly go wrong?
Quite a lot.

What is a DTO?
Section titled “What is a DTO?”A DTO is a data transfer object.
It describes how data gets transported across a technical boundary. Between backend and frontend, for example.
A DTO is therefore not a UI model. It’s not a domain model. It’s not a ViewModel. It’s a transport model.
That’s an important difference.
A DTO answers the question:
How does the information get across the wire?
A ViewModel, on the other hand, answers the question:
What does the UI need to represent its state correctly and understandably?
When both questions get answered with the same data structure, coupling follows.
What DTO leakage looks like
Section titled “What DTO leakage looks like”A typical example:
// API returns:interface TaskDto { task_id: string; created_at: string; // ISO string assigned_to_user_id: string | null;}And this DTO lands straight in the component:
@Component(...)export class TaskCardComponent { @Input() task: TaskDto; // ← DTO in the UI layer}Maybe even in the template too:
<article> <h3>{{ task.task_id }}</h3>
<p>Created on: {{ task.created_at | date }}</p>
<p>Assigned to: {{ task.assigned_to_user_id ?? 'Unassigned' }}</p></article>That looks harmless.
But several boundaries are already being violated here:
- The component knows API field names.
- The template knows technical null semantics.
- Date formatting depends on a transport format.
- UI text is derived from API structure.
- Backend changes can break UI code.
That’s DTO leakage.

Why it’s so tempting at first
Section titled “Why it’s so tempting at first”Using DTOs directly feels efficient.
You skip a mapping step. You skip another interface. You seemingly save code.
And sometimes that’s even fine.
If you’re building a small prototype, an internal throwaway tool, or a very thin surface over a stable API, using DTOs directly can be defensible.
The problem starts when this turns into architecture.
At that point, the DTO is no longer just a transport object. It suddenly becomes the silent source of truth for the UI.
And from that moment on, the API structure has a say in your template.
That’s the point where pragmatism slowly tips over into coupling.
What happens when DTOs leak through?
Section titled “What happens when DTOs leak through?”DTO leakage rarely produces one big bug right away.
It produces a lot of small ones.
The API changes a field name?
task_id -> idSuddenly components, templates, tests, and maybe even sorting logic break.
The backend team switches from snake_case to camelCase?
created_at -> createdAtThen the great search through components and templates begins.
An ISO string is supposed to be treated as a Date?
created_at: string;Then formatting or parsing logic ends up somewhere in the component.
A null means “unassigned” in business terms?
assigned_to_user_id: null;Then business logic ends up sitting in the template.
And eventually you see code like this:
@if (task.assigned_to_user_id) {<span>{{ getUserName(task.assigned_to_user_id) }}</span>} @else {<span>Unassigned</span>}The problem isn’t this one line.
The problem is that the UI starts interpreting API semantics.
The real problem: the dependency points the wrong way
Section titled “The real problem: the dependency points the wrong way”DTO leakage means: the UI takes its cues from the API.
That sounds normal enough at first. After all, that’s where the data comes from.
Architecturally, though, it’s dangerous.
The UI shouldn’t have to know whether the backend delivers task_id, taskId, id, or identifier. The UI needs a task with an ID.
The UI shouldn’t have to know whether a date arrives as an ISO string, a timestamp, or a nested object. The UI needs a displayable date.
The UI shouldn’t have to know that null means “unassigned” in business terms. The UI needs a piece of text it can display.
The API belongs to infrastructure. The component belongs to presentation. Between them, you need translation.

The alternative: ViewModel
Section titled “The alternative: ViewModel”The component shouldn’t know about TaskDto.
It should get a model built for presentation.
interface TaskViewModel { id: string; createdAt: Date; assignee: string;}The ViewModel doesn’t describe how data gets transported.
It describes what the UI needs.
The translation happens at a clear boundary:
function toTaskViewModel(dto: TaskDto): TaskViewModel { return { id: dto.task_id, createdAt: new Date(dto.created_at), assignee: dto.assigned_to_user_id ?? 'Unassigned', };}After that, the component only knows about the ViewModel:
@Component(...)export class TaskCardComponent { @Input() task!: TaskViewModel;}And the template gets a lot more boring:
<article> <h3>{{ task.id }}</h3>
<p>Created on: {{ task.createdAt | date }}</p>
<p>Assigned to: {{ task.assignee }}</p></article>Boring code is often good code.
Not because it’s trivial. But because it no longer has to make decisions in the wrong place.
The mapper isn’t a tedious chore
Section titled “The mapper isn’t a tedious chore”Mappers often get dismissed as unnecessary boilerplate.
That’s sometimes true.
When a mapper just copies fields one-to-one, it feels like a ritual.
function toViewModel(dto: TaskDto): TaskViewModel { return { id: dto.id, title: dto.title, };}But even then, it serves an important function:
It marks a boundary.
Today it might just copy. Tomorrow it translates a date. The day after, it normalizes optional values. Next week, it protects 15 components from an API change.
A mapper isn’t just code.
It’s a deliberate seam in the architecture.
When the API changes, something should break exactly there. Not everywhere.

What belongs in a ViewModel?
Section titled “What belongs in a ViewModel?”A good ViewModel isn’t just a renamed DTO.
It’s tailored to presentation.
Typical jobs of a ViewModel:
- translating technical field names into UI names
- converting date values into the right types or display formats
- handling
nullandundefinedin business terms - resolving IDs into readable names
- precomputing display states
- deriving permissions for UI actions
- providing loading and error states consistently
- removing complex conditionals from the template
Example:
interface TaskViewModel { id: string; title: string; createdAtLabel: string; assigneeLabel: string; canEdit: boolean; isOverdue: boolean;}Then the template no longer needs to know how “overdue” gets calculated.
@if (task.isOverdue) {<p class="warning">Overdue</p>}The template consumes a decision.
It doesn’t make the decision itself.
But isn’t that duplicate work?
Section titled “But isn’t that duplicate work?”Sometimes, yes.
But the alternative is often not “no work.”
The alternative is just that the work gets spread around.
A little mapping in the template. A little null-handling in the component. A little date logic in a pipe. A little API knowledge in the test. A little edge case in a second component.
That feels faster in the short term, because nobody writes a mapper.
Long term, it’s slower, because nobody knows anymore where the translation actually happens.
Explicit mapping is visible work.
DTO leakage is hidden work.
When is it okay to use a DTO directly?
Section titled “When is it okay to use a DTO directly?”Not every use of a DTO is automatically an architectural crime.
There are cases where using it directly can be fine:
- very small prototypes
- throwaway code
- internal admin tools with an extremely short lifespan
- generated API clients that stay confined to infrastructure
- views that really just display raw API data
- very stable APIs with a deliberately identical UI contract
But once business-meaningful presentation enters the picture, I’d get cautious.
A good rule of thumb:
If the template knows API field names, technical null values, or transport formats, the boundary is probably leaking.
Where should the mapping happen?
Section titled “Where should the mapping happen?”Not in the component.
The component is a consumer. It shouldn’t have to know how task_id becomes id.
The mapping belongs in an adapter, domain, or application layer. The exact name matters less than the direction.
One possible layout:
Infrastructure API Client DTOs
Application / Domain Mapper ViewModel Facade / Store
Presentation Component TemplateOr as a flow:
TaskDto -> toTaskViewModel(dto) -> TaskViewModel -> Component -> TemplateThe component gets the result of the translation.
Not the raw material.

Tests get simpler
Section titled “Tests get simpler”DTO leakage often makes tests unnecessarily broad.
When components know about DTOs, component tests have to recreate API structures.
const task: TaskDto = { task_id: '123', created_at: '2026-06-19T10:00:00Z', assigned_to_user_id: null,};The test then indirectly tests API semantics, mapping, null-handling, and UI behavior all at once.
With ViewModels, the component test gets smaller:
const task: TaskViewModel = { id: '123', createdAt: new Date('2026-06-19T10:00:00Z'), assignee: 'Unassigned',};And the mapper gets its own focused 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: 'Unassigned', }); });});That’s the real payoff:
- Components test presentation.
- Mappers test translation.
- Infrastructure tests test the API connection.
Each layer gets tested for exactly what it’s responsible for.
The benefit
Section titled “The benefit”When the API changes, the mapper changes.
Not the component.
Not the template.
Not every test.
Not every spot that happened to read created_at at some point.
That’s not academic purity.
That’s maintainability.
DTOs are allowed to exist.
They just can’t live everywhere.
Conclusion
Section titled “Conclusion”DTO leakage is dangerous because it looks pragmatic.
You skip an interface. You skip a mapper. You skip a decision.
But you pay for it later with coupling.
Good frontend architecture doesn’t mean inventing as many models as possible. It means deliberately drawing boundaries.
A DTO belongs at the technical boundary.
A ViewModel belongs to the UI.
And somewhere in between sits the translation.
Not because architecture is supposed to look pretty.
But because otherwise changes end up right where they’re most expensive: in components, templates, and tests.
The short rule:
DTOs in. ViewModels out. Components stay clean.