Recently, for one of our clients, we encountered one interesting problem. Their stack is Rails + React, where Rails is for providing APIs, and React is for consuming those APIs. Frontend team prefers API payloads to be in camelCase. There are existing npm modules which can automatically convert snakecase to camelCase, but (un)fortunately front-end team is not using those.
We use AMS (active model serializers) to generate json responses. We are
dealing with Rails 3, and app cannot be upgraded :). Latest version of AMS has
out of box support for transformation. Say if we have to expose attributes of
model Post
, we have this code:
Using camelCase in ruby code looks ugly, but legacy code responds with camelCase for apis. One (obvious) solution is to request frontend team to use npm packages to convert snakecase to camelCase for GET requests, and camelCase to snakecase for all POST requests. Making that change would involve touching lots of components, which we didn't want to do right away. We looked for alternatives and figured out this approach.
Let me show you how the tests for the Post object described above look, so that you get undestand what the problem is:
If you look at specs, generally we don't use camelCase in ruby to write specs.
The code tries to map authorName
to author_name
, lots of copying going
around. Let's go step-by-step in improving this situation:
Step 1: Make snakecase/camelCase configurable via urls
We modified all APIs to support a param called snakecase
. If this query
param is set, APIs are served in snakecase, otherwise they are served in
camelCase. So, modified specs look like this:
And AMS also looks sane, ie it looks like this:
Bit of faith restored for ruby developers. APIs typically look like this now:
- for a GET request:
GET /posts/10?snakecase=1
- for a PUT request:
PUT /posts/10?snakecase=1
(body contains payload)
But frontend still expects payload to be in camelCase. It's simple, avoid the
query param snakecase=1
and we are all good.
Step 2: Implement snakecase/camelCase intelligence in controller params
Some problems can be solved with another level of indirection. We are going
to play with controller level params
, and provide one nice wrapper around it.
We are not going to use params
directly in our controllers, we are going
to use a wrapper called api_params
. Before, we jump into code, we have to
support usecases like these for frontend:
- filtering posts:
GET /posts?authorName=Jon&page=2&perPage=10
- updating a post:
PUT /posts/10
, body has:{ title: 10, authorName: 'Ben' }
Note those camelCases in urls, and POST/PUT body. Now, onto code:
Ignoring specifics of code, instead of using params
in controller, we are
using api_params
. What this does is:
- If
snakecase=1
, it means all the params are already in snakecase, and can be directly consumed by Ruby/Rails code. - If
snakecase
is not set (our frontend case), we assume that all the params are in camelCase, and they have to be converted to snakecase before Ruby/Rails code consumes it.
snakecase_params
method is interesting, and simple. All it has to do is to
perform a deep key transformation. Basecamp has already written some code to
deep transform hashes. Code can be found in deep hash transform repo.
We are going to re-use that code. Code looks like this:
Now, we can inject this concern into our ApisController
, and boom, we have
snakecase_params
helper. Now, we have backend developers happy with using
snakecase, and frontend developers are happy working with camelCase. What
else is left? Yes, automatically transforming payload for GET requests.
Step 3: Teaching AMS to be aware of snakecase/camelCase
Remember, our AMS for Post
is still using snakecase. Now based on url params,
we have to transform attributes. Let's take a look at AMS for Post
again:
AMS makes it easy to play with attributes
, again with another level of
indirection.
First, we will use serializer options at controller level to tell AMS to transform payload to camelCase or snakecase.
All AMSes will now pickup default_serializer_options
from controller. In
these default options, we are appending snakecase
option into AMS world.
Now, how do we tell AMS to transform payload to camelCase or snakecase?
Its simple: Use a base serializer, and derive all serializers from it.
We have to implement camelize
now. We will extend ApiConventionsHelper
and
implementcamelize
Finally
With bit of conventions in place, and playing around controllers, and AMS, we are able to keep both backend developers and frontend developers happy. Interesting point to note here is, there is hardly any meta programming.
Thanks for reading! If you would like to get updates about subsequent blog posts Codemancers, do follows us on twitter: @codemancershq.