At work, we’ve been building front-end applications with React and Redux. Our back ends are generally written in Rails.

In order to make the API calls to the back end, we’ve been using the redux-api-middleware library which works quite well for us. We’ve added a bit of infrastructure that makes communicating with a Rails server a bit easier.

Another library we use quite a bit is Ramda. I talked about that in Using Ramda With Redux.

As with some of my earlier articles, this code is part of Zeal’s react-boilerplate.

redux-api-middleware

redux-api-middleware is a Redux middleware that handles specially-formatted Redux actions. It exposes a special property name (CALL_API) and any action that contains that property is handled by the middleware. The value of CALL_API is a descriptor that specifies how an API call should be made and what Redux actions to dispatch at various stages of the process. The possible actions are a REQUEST action that is dispatched just before making the API call, a SUCCESS action that is dispatched when the call succeeds, and a FAILURE action that is dispatched when the call fails.

Here’s a minimal example of a redux-api-middleware action, taken from the README:

Minimum API Middleware Action
import { CALL_API } from `redux-api-middleware`;
{
[CALL_API]: {
endpoint: 'http://www.example.com/api/users',
method: 'GET',
types: ['REQUEST', 'SUCCESS', 'FAILURE']
}
}

Problems to Solve

As we began working with Redux and Rails, we started running into a fair bit of duplicated code and wanted a way to extract it into one place. Duplicated pieces included:

  • Converting from JavaScript-style camelCase keys to Rails-style snake_case keys.
  • Converting payloads to “stringified” JSON.
  • Adding common request headers

Solution

The version of this code in the react-boilerplate repository is somewhat more involved than this, as it solves a couple of other problems and also injects some dependencies to make testing easier. I’m leaving those out to make the code easier to understand.

To eliminate duplication, we introduced a utility function, callApi, that we use when making calls to our Rails application’s API:

callApi
export function callApi(callDescriptor) {
return {
[CALL_API]: tranformCallDescriptor(callDescriptor)
}
}

callApi takes a redux-api-middleware call descriptor, transforms it, and returns a Redux action that can be dispatched through the middleware. I’ll talk about transformCallDescriptor in a moment.

We can already see that none of the callers of callApi need to worry about the CALL_API property name; that’s now encapsulated in this function. The callers still need to know the format of the call descriptor, but they are now slightly less coupled to redux-api-middleware.

A simple use of callApi in a Redux action creator might look like this:

Using callApi
export function fetchUser(id) {
return callApi({
endpoint: `/api/v1/users/${id}`,
types: [
ActionTypes.FETCH_USER_REQUEST,
ActionTypes.FETCH_USER_SUCCESS,
ActionTypes.FETCH_USER_FAILURE,
]
})
}

transformCallDescriptor looks like this:

transformCallDescriptor
function tranformCallDescriptor(callDescriptor) {
const transform = compose(
evolve({
body: compose(JSON.stringify, decamelizeKeys),
types: compose(
adjust(tranformSuccessPayload, 1),
adjust(transformFailurePayload, 2)
)
}),
merge({
method: 'GET'
}),
mergeWith(merge, {
headers: {
'Content-Type': 'application/json'
}
})
)
return transform(callDescriptor)
}

As I write this, I see that I could extract some named helper functions that would make this a bit clearer. What is this doing?

First, we use some Ramda functional magic to define a local transform function and then we apply that to the call descriptor.

  • We use compose to build up a function that applies multiple transformations. Saying compose(f, g, h)(argument) is the same as saying f(g(h(argument))). We call h with the argument, pass the result of that to g, the result of that to f, and return the result of that as the final result.

  • Reading from the bottom up, we supply a default header that sets the Content-Type to application/json. We could just use merge here, but then any headers provided by the caller would overwrite our header. Instead, we want to merge caller-supplied headers with our header, so we use mergeWith and merge. The caller can provide its own Content-Type header and that will be used, but if it doesn’t, we’ll use our default value.

  • Next, we default the HTTP method to GET. Again, the caller can provide a different method, but if none is given, we default to GET.

  • Finally, we use Ramda’s evolve function to modify some of the properties passed in by the caller. evolve takes an object that specifies a transformation function for each key. Here we’re saying we want to transform the body and the types properties.

  • We transform the body by converting all of the keys from camelCase to snake_case using the humps library’s decamelizeKeys function. We then take the result and call JSON.stringify to convert the body to a JSON string.

  • We then transform several of the elements of the types array using Ramda’s adjust function. types is the array of three action type descriptors that I mentioned above. The first specifies the REQUEST action that will be dispatched by redux-api-middleware; the second specifies the SUCCESS action; and the third the FAILURE action. See the Lifecycle documentation of redux-api-middleware for more details.

Let’s look at how we transform the SUCCESS action first:

transformSuccessPayload
function tranformSuccessPayload(type) {
return {
...objectize(type),
payload: (action, state, res) =>
getJSON(res).then(camelizeKeys)
}
}
function objectize(type) {
return contains(typeof type, ['string', 'symbol'])
? { type }
: type
}

As described in Customizing the dispatched FSAs documentation, redux-api-middleware allows the SUCCESS type descriptor to be a string or symbol OR an object that serves as a blueprint for creating the Redux action.

We’re not sure which version our callers will supply, so we have a little utility function called objectize that ensures we have an object. contains is another Ramda function.

We then merge in a new payload property using ES7 object-spread notation. We could have used Ramda’s merge here as well but for simple things like this, I find object-spread more readable.

The new payload property is a function that does exactly what redux-api-middleware would do (using its getJSON utility function), but then uses humps’ camelizeKeys to transform the keys in the response from snake_case to camelCase. This is the reverse transformation of the one we applied to the body above.

transformFailurePayload is almost identical, but we need to wrap the transformed response in a redux-api-middleware ApiError to match the default behavior:

transformFailurePayload
function transformFailurePayload(type) {
return {
...objectize(type),
payload: (action, state, res) => getJson(res).then(json =>
new ApiError(res.status, res.statusText, camelizeKeys(json))
)
)
}

With callApi, we can now simplify the code we use to make API calls and remove a bunch of copy-pasted logic.

We’ve added other transformations on some of our client projects. For example, in one case we also transform the endpoint property to prepend a hostname and common URL prefix. In another case, we look up the API authorization token and merge in an Authorization header along with the Content-Type header described above.

This little set of utility functions allows for a lot of common boilerplate to be extracted to one place instead of being scattered all over the code.