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.
Adapter pattern to the rescue
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
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
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.
So after you’ve created some adapter functions, how should you call them? You should isolate the calls to your adapters in your modules that handle making API calls. This helps to keep things decoupled since you are minimizing the amount of things in your application that even know adapters exist.
The API calling functions will use the adapters internally, and should have the UI data model going both in and out. Their cohesion with the adapters is so high that they could be bundled together in the same module. When your data gets into your application state, it will already be in the shape designed for the UI.
Product requirements and API endpoints will almost inevitably change, sometimes frequently. Even if they don’t, you will probably end up wanting to refactor your React components or other portions of your application at some point. It’s much easier to handle these situations gracefully when your view logic is very simple and decoupled from your API logic.
Implementing the adapter pattern will help you to decouple your modules and make it easier to change them without unexpected side effects.
If you’re interested in learning more background on SendGrid’s front end test automation software, check out Choosing a Front-End Test Automation Framework.