React|Architecture and Design

Frontend DTOs: Building a Mapper Layer Unshaken by Backend API Changes

6

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.

typescript
// ❌ 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.

  • DTO (Data Transfer Object): Exactly as the data comes down from the server. (No compromise.)
  • Model (Domain Entity): A form that is easy to use in the frontend. (Follows our rules.)
  • typescript
    // 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.

    typescript
    // 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).

    typescript
    // 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."

    typescript
    // 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

  • Unified Naming Convention: Even if the server uses snake_case, the frontend can maintain camelCase.
  • Null Safety: By handling default values (?? '') for null or undefined in the mapper, you can reduce optional chaining hell (e.g., user?.profile?.url) in components.
  • Centralized Data Processing: Logic like date formatting or currency commas is handled in the mapper, not the view, simplifying view logic.
  • 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.

    typescript
    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

  • Problem: Using API responses (DTOs) directly in components makes the frontend vulnerable to backend changes.
  • Solution: Distinguish between DTO (Server Form) and Model (UI Form), and convert between them using a Mapper function.
  • Effect:

  • 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)”

    🔗 References

  • Martin Fowler - Data Transfer Object
  • Comments (0)

    0/1000 characters
    Loading comments...