Frontend DTOs: Building a Mapper Layer Unshaken by Backend API Changes
One peaceful afternoon, while Chris was coding, a backend developer approached him with a bombshell announcement.
"Hey Chris, we changed the DB schema a bit. In the API response, user_name is now fullName, and the address field has been moved inside a location object. Please update your code!"
Chris's face turned pale. There are over 50 components currently using user_name. Using "Find and Replace" is bound to cause errors, and working overtime tonight is now a certainty.
Why is frontend code so vulnerable to backend API changes?
It's because we used the server's raw data directly in our components.
Today, we will learn about DTOs (Data Transfer Objects) and the Mapper Pattern, which act as a seawall preventing server changes from flooding into the frontend UI.
1. The Problem: Dangerous Cohabitation with Server Data
Typically, we define interfaces exactly matching the API response types.
// ❌ Types dependent on server naming and structure
interface UserResponse {
user_id: number; // Snake Case
user_name: string;
created_at: string; // String like "2024-01-01"
is_active: 0 | 1; // 0 or 1 instead of boolean
}
function UserProfile({ user }: { user: UserResponse }) {
// Component code is polluted by server style
return (
<div>
{/* Snake case invades JS code */}
<h1>{user.user_name}</h1>
{/* Date formatting logic mixed into View */}
<span>{new Date(user.created_at).toLocaleDateString()}</span>
</div>
);
}The problem with this approach is that if the server API spec changes, you have to tear down and rewrite all the UI component code. The Coupling is too high.
2. The Solution: Mapper, the Data Translator
To solve this, we need to separate "Data given by the server (DTO)" from "Data I want to use (Domain Model)" and place a Translator (Mapper) in between to convert them.
Step 1: Define DTO and Domain Model
First, define two types.
// types/user.ts
// 1. DTO: Reflects the server's reality (Allows Snake Case)
export interface UserDTO {
user_id: number;
user_name: string;
created_at: string;
is_active: 0 | 1;
profile_img?: string | null; // Server data is uncertain
}
// 2. Model: Ideal form we want to use (Camel Case)
export interface User {
id: number;
name: string;
createdAt: Date; // Date object instead of string
isActive: boolean; // Boolean instead of 0/1
profileUrl: string; // Empty string instead of null (Safe)
}Step 2: Write the Mapper Function
Now, create a pure function that converts DTO to Model. This is your seawall.
// mappers/userMapper.ts
import { User, UserDTO } from '../types/user';
export const userMapper = (dto: UserDTO): User => {
return {
id: dto.user_id,
name: dto.user_name,
// Perform date conversion logic once here
createdAt: new Date(dto.created_at),
// 0/1 -> boolean conversion
isActive: dto.is_active === 1,
// Null check and default value handling (Makes View easier)
profileUrl: dto.profile_img ?? 'https://default-image.com',
};
};Step 3: Apply
Perform the mapping at the point of fetching data (API Layer).
// api/userApi.ts
export const fetchUser = async (id: number): Promise<User> => {
const response = await fetch(`/api/users/${id}`);
const dto: UserDTO = await response.json();
// ✨ Convert here! The component won't even know the DTO exists.
return userMapper(dto);
};3. Benefits of the Mapper Pattern
Now, the backend developer returns.
"Chris, user_name has been changed to fullName!"
Chris smiles and replies.
"No worries. I just need to fix one line in the mapper file."
// mappers/userMapper.ts
export const userMapper = (dto: UserDTO): User => {
return {
// ...
name: dto.fullName, // Only need to edit here!
// ...
};
};Since the UI component (UserProfile) is still looking at user.name, there is no need to touch any of the 50 components.
Additional Benefits
4. Practical Tip: Using Libraries (Zod)
TypeScript interfaces disappear at runtime. If the server breaks its promise and sends the wrong type, the app can crash.
Using a runtime validation library like Zod with your mapper makes it much safer.
import { z } from 'zod';
// Define schema with Zod (For runtime validation)
const UserDTOSchema = z.object({
user_id: z.number(),
user_name: z.string(),
// ...
});
export const fetchUser = async (id: number): Promise<User> => {
const res = await fetch(...);
const json = await res.json();
// Verify data against schema (Throws error if failed)
const dto = UserDTOSchema.parse(json);
return userMapper(dto);
};Key Takeaways
We have learned how to safely transform data.
But how should we organize these DTOs, Models, Mappers, and View components in our folder structure? Let's apply Clean Architecture to React so we don't get lost even in massive projects.
Continuing in: “React Clean Architecture: A 3-Layer Separation Strategy (Domain, Data, Presentation)”