Who Controls Your Frontend?

Is it the frontend devs or the backend devs? Read to find out

What do I mean by "who controls your frontend"?

TLDR: If the API contract changes, and the frontend has to make edits to the component layer as a result, the backend controls your frontend. This becomes a problem if say 5 different components relied on API data that has now changed, as you now must make edits in 5 different files. However, if the frontend only has to update one file in it's API layer to account for new API contract changes, the frontend controls the frontend and this is what we want.

The Problem

Lets set the stage with a simple example.

There is a getAccount api and a mobile client calls this api to render an Account screen.

The getAccount api returns an Account model like so:

type ApiAccount = {
    /** firstname + lastname */
    username: string    
}

The Account screen is composed of many components that directly use the ApiAccount.username field. This is a common experience in the few apps I've worked on; directly using api data throughout the component layer, and this is what we'd like to avoid.

type AccountProps = {
  account: ApiAccount
}

function AccountHeader({ account }: AccountProps) {
   return (
       <h1>{account.username}</h1>
   )
}

function EditAccountForm({ account }: AccountProps) {
   return (
       <h1>{account.username}</h1>
   )
}

All of a sudden, the API team decides to change the ApiAccount model to this...

type ApiAccount = {
  firstName: string
  lastName: string
}

What this means now is most frontends will have to go through each of their Account screen components and update account.username to ${account.firstName} + ${account.lastName}.

This is what I mean by the backend controls the frontend. Anytime the backend makes a change, the frontend responds, which is fine! The problem however is how big the response is, how many different files may need to be changed as a result of directly using API data in the component layer.

The Solution

The frontend should fetch API data and directly transform it to internally defined models.

As a matter of fact, the first step before creating any UI should be defining custom models for the frontend that make building it easy for frontend devs!

Lets do that with our simple Account screen example

type AccountModel = {
  /** the frontend just needs one string with a name */
  name: string;
}

Now that we have our custom defined model, we make the api call and transform the api data to our own model.

async function getAccount(id: string): AccountModel {
  const apiData = await api.get(`/account/${id}`)
  return transform(apiData)
}

The transform function in this case would look something like this

// transform function for v1
function transform(apiData: ApiAccount): AccountModel {
  return {
    name: apiData.username
  }
}

// transform function for v2
function transform(apiData: ApiAccount): AccountModel {
  return {
    name: `${apiData.firstName} ${apiData.lastName}`
  }
}

Anytime the API contract changes, the frontend at this point just has to edit the respective transform function to account for the apiData that changed.

Like I said, anytime the backend changes the API contract, the frontend will have to respond, but we want to minimize that response and that's what this accomplishes. We only need to update the frontend in one file, where we fetch from the API in the transform function.

We isolate API contract changes to the frontend API layer and don't let them leak into the component layer.

All of our components should be using our internally defined models, so we don't even technically have to re-test the frontend as no code in the component layer is edited.

Here is what the new dummy Account components look like

type AccountProps = {
  account: AccountModel
}

function AccountHeader({ account }: AccountProps) {
   return (
       <h1>{account.name}</h1>
   )
}

function EditAccountForm({ account }: AccountProps) {
   return (
       <h1>{account.name}</h1>
   )
}

It doesn't seem like much of a difference, but the components are using the models defined just for them. The frontend owns the frontend. A change to the API contract doesn't mean there is a change to the component layer.

Bonuses

Here are some extra bonuses (aside from easy refactors) when taking this approach of transforming API models to custom models the frontend understands

  • Less bulky models in component layer as API models usually contain unused fields. This can make mocking components easier in stuff like storybook
  • convert date fields to actually use Date instead of string. Servers cant transport JS Dates!
  • Better clearer/preciser field names. API devs usually name or break things out in a way that doesn't make sense for frontend devs