Using redux-api-middleware with Rails
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:
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
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:
transformCallDescriptor
looks like this:
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. Sayingcompose(f, g, h)(argument)
is the same as sayingf(g(h(argument)))
. We callh
with the argument, pass the result of that tog
, the result of that tof
, 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
toapplication/json
. We could just usemerge
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 usemergeWith
andmerge
. The caller can provide its ownContent-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 toGET
. -
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 thebody
and thetypes
properties. -
We transform the
body
by converting all of the keys from camelCase to snake_case using the humps library’sdecamelizeKeys
function. We then take the result and callJSON.stringify
to convert the body to a JSON string. -
We then transform several of the elements of the
types
array using Ramda’sadjust
function.types
is the array of three action type descriptors that I mentioned above. The first specifies theREQUEST
action that will be dispatched by redux-api-middleware; the second specifies theSUCCESS
action; and the third theFAILURE
action. See the Lifecycle documentation of redux-api-middleware for more details.
Let’s look at how we transform the SUCCESS
action first:
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:
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.