Note: This post comes from SendGrid's engineering team. For more technical engineering posts like this, check out our technical blogroll.
Have you ever been near the end of building some complex new set of functionality, and the requirements or API contracts you were counting on changed suddenly? I think we’ve all been there.
It can be really disappointing to have the work you spent so much time on go to waste, especially if you took care to do a great job. But it doesn’t have to sting as hard if you plan for change ahead of time and organize your code accordingly.
At SendGrid, we are currently building our systems in AWS to deliver the next generation of Marketing Campaigns
features. It's a very exciting time and we are learning a lot along the way. We are also taking this opportunity to rethink how some of our systems and APIs are designed. And although our latest APIs are not set in stone, we still need to create a robust client application that will ultimately consume them.
I'd like to share what our approach has been for dealing with this situation gracefully so that it is easier to isolate the portions of code that must be refactored when requirements or APIs change.
To help deal with the uncertainty about the final contracts our APIs will define, we have decided to apply the adapter design pattern to our React application. An adapter is essentially just a translation layer that connects two different interfaces together. The interfaces we are connecting in this situation are:
- The data model required by our React components
- The data model defined by our APIs
[caption id="attachment_31641" align="aligncenter" width="631"]
Diagram of how the adapter layer relates to other components of the application[/caption]
In the past, I have more or less fed raw JSON from the API directly into my React components, leaving it to the view layer to figure out how to handle the state. Problems with this approach include:
- React components become coupled to the API
- React components are harder to maintain
- The view layer becomes more bloated
- Using new versions of API endpoints will break your views
Eventually, I decided that having the view so tightly coupled to the API was a poor decision. I wanted to decouple things not just in case I needed to use a new version of an API, but for better code quality in general. The main point of the adapter pattern is to decouple your view from your APIs. Advantages of this approach:
- Reduced coupling between modules in your application
- Easier maintenance of React components and API libraries
- The view layer becomes focused solely on deciding what a given state should look like
- Updating your application to work with new versions of APIs becomes very simple since you only need to update your adapter and your API library
- Thinking about different types of data models for the UI and the API helps you plan out your modules and components in a logical way
Implementing the adapter pattern can be as simple as creating a library of pure functions which translate UI objects into API objects, and vice versa. They can then be very heavily unit tested. I highly recommend using TypeScript for this because it makes refactoring data structures much easier, and it encourages you to think about your UI and API data models separately.
Here is a somewhat contrived example for what an adapter might look like. Notice the API returns a nested data structure, which isn’t necessarily for convenient to use with React components.