The Shield Against Backend Turbulence

Posted by Amir on January 01, 2024 · 5 mins read

You push a new update, and your app is shining—until the backend team updates their APIs. Now, your app's features start breaking. What if there was a way to shield your app from such disruptions? This isn't about overhauling your entire architecture overnight; it's about starting with a single, manageable step drawn from Uncle Bob's Clean Architecture principles.

Context and Background:

Implementing the full spectrum of Clean Architecture can seem daunting, and that's not our goal today. Instead, let's tackle one specific pain point: the ripple effect of changes in the Backend API layer. It's a simple task, a starting point for bigger things.

Why This First Step Matters:

  • It directly addresses the immediate pain of backend changes forcing unwanted frontend revisions.
  • It sets the foundation for a more resilient and flexible app architecture.
  • It's an attainable goal, regardless of the size or stage of your project.

The Solution: Abstraction and Adapter Layer:

To create an app that stands independent of backend fluctuations, we need a shield—an adapter layer that abstracts away the backend's Request DTOs and Response DTOs.

Before the Adapter Layer:

Here's a familiar scene: our app's user profile fetch function breaks with a backend change.

        
interface UserProfile {
  id: string;
  name: string;
  email: string;
  // ... other properties exactly as they come from the API
}

const UserProfileComponent: React.FC<{ userId: string }> = ({ userId }) => {
  const [userProfile, setUserProfile] = useState(null);

  useEffect(() => {
    const fetchUserProfile = async () => {
      try {
        const response = await fetch(`https://api.example.com/users/${userId}`);
        const userProfile: UserProfile = await response.json();
        setUserProfile(userProfile);
      } catch (error) {
        console.error("Failed to fetch user profile:", error);
      }
    };

    fetchUserProfile();
  }, [userId]);

if (!userProfile) {
    return //SomeLoading
  }

  return //SomeComponent
};

export default UserProfileComponent;
        
    

Introducing the Adapter Layer:

Now, let's isolate our app with its own data models. Check out this approach:

        
// UserApiClientService handles the API calls
class UserApiClientService {
  private baseUrl: string;

  constructor(baseUrl: string) {
    this.baseUrl = baseUrl;
  }

  async fetchProfile(userId: string): Promise {
    const response = await fetch(`${this.baseUrl}/users/${userId}`);
    if (!response.ok) {
      throw new Error('Failed to fetch user profile');
    }
    return response.json();
  }
}

// UserApiAdapterService converts between API and App models
class UserApiAdapterService {
  static fromApiProfile(apiProfile: UserApiProfile): AppUserProfile {
    return {
      id: apiProfile.user_id,
      fullName: `${apiProfile.first_name} ${apiProfile.last_name}`,
      emailAddress: apiProfile.email,
      // ... other conversions
    };
  }
}

// UserApiProfile reflects the API response structure
interface UserApiProfile {
  user_id: string;
  first_name: string;
  last_name: string;
  email: string;
  // ... other properties as they come from the API
}

// AppUserProfile reflects the app's internal model structure
interface AppUserProfile {
  id: string;
  fullName: string;
  emailAddress: string;
  // ... other properties as needed by the app
}

const UserProfileComponent: React.FC<{ userId: string }> = ({ userId }) => {
  const [userProfile, setUserProfile] = useState(null);
  const apiClient = new UserApiClientService('https://api.example.com');

  useEffect(() => {
    const fetchUserProfile = async () => {
      try {
        const userApiProfile = await apiClient.fetchProfile(userId);
        const appUserProfile = UserApiAdapterService.fromApiProfile(userApiProfile);
        setUserProfile(appUserProfile);
      } catch (error) {
        console.error("Failed to fetch user profile:", error);
      }
    };

    fetchUserProfile();
  }, [userId, apiClient]);

  if (!userProfile) {
    return //SomeLoading
  }

  return //SomeComponent
};

export default UserProfileComponent;
        
    
As you see, our app now speaks in its own language. Backend DTOs no longer dictate our app's data structure. We define our app models, and the adapter layer does the translation work.

Applying the Approach:

This method is a game-changer for both new and existing projects. It's straightforward and doesn't pile on extra work. For existing projects already tied to backend models, begin with creating adapter services for your use cases. Initially, let these adapters simply pass through the data. This grants you an intermediary layer. Gradually, you can evolve your app's models to serve the app's efficiency, not just to mirror the backend.

Conclusion:

Starting with an adapter layer might seem like a small step, but it's a leap towards a robust, Clean Architecture. It's about making your client app not just independent, but resilient and ready for the future.