March 12, 2024
In the world of web development, conventions often differ between backend and frontend technologies. This becomes evident when comparing variable naming case conventions used in Ruby on Rails (snake case) and JavaScript (camel case). At neeto, this difference posed a major hurdle: the requirement for manual case conversion between requests and responses. As a result, there was a significant amount of repetitive code needed to handle this conversion.
Here’s a snippet illustrating the issue faced by our team:
// For requests, we had to manually convert camelCase values to snake_case.
const createUser = ({ userName, fullName, dateOfBirth }) =>
axios.post("/api/v1/users", {
user_name: userName,
full_name: fullName,
date_of_birth: dateOfBirth,
});
// For responses, we had to manually convert snake_case values to camelCase
const {
user_name: userName,
full_name: fullName,
date_of_birth: dateOfBirth,
} = await axios.get("/api/v1/users/user-id-1");
This manual conversion process consumed valuable development time and introduced the risk of errors or inconsistencies in data handling.
To streamline our workflow and enhance interoperability between the frontend and backend, we made a decision to automate the case conversion.
Implementing automatic case conversion across Neeto products required a thoughtful approach to minimize disruptions and ensure a smooth transition. Here's how we achieved this goal while minimizing potential disruptions:
We created a pair of Axios interceptors to handle case conversion for requests and responses. The interceptors were designed to recursively convert the cases, managing the translation between snake case and camel case as data traveled between the frontend and backend. This smooth transition simplified the workflow, cutting out the requirement for manual case conversion in most situations.
To do a smooth rollout without breaking any products and due to certain special
APIs requiring specific case conventions due to legacy reasons or external
dependencies, we introduced custom parameters transformResponseCase
and
transformRequestCase
within Axios. These parameters allowed developers to
opt-out of the automatic case conversion for specific API endpoints. By
configuring these parameters appropriately, we prevented unintentional case
conversions where needed, maintaining compatibility with APIs that required
different conventions.
This is how we crafted our axios interceptors:
import {
keysToCamelCase,
serializeKeysToSnakeCase,
} from "@bigbinary/neeto-cist";
// To transform response data to camel case
const transformResponseKeysToCamelCase = response => {
const { transformResponseCase = true } = response.config;
if (response.data && transformResponseCase) {
response.data = keysToCamelCase(response.data);
}
return response;
};
// To transform error response data to camel case
const transformErrorKeysToCamelCase = error => {
const { transformResponseCase = true } = error.config ?? {};
if (error.response?.data && transformResponseCase) {
error.response.data = keysToCamelCase(error.response.data);
}
return error;
};
// To transform the request payload to snake_case
const transformDataToSnakeCase = request => {
const { transformRequestCase = true } = request;
if (!transformRequestCase) return request;
request.data = serializeKeysToSnakeCase(request.data);
request.params = serializeKeysToSnakeCase(request.params);
return request;
};
// Adding interceptors
axios.interceptors.request.use(transformDataToSnakeCase);
axios.interceptors.response.use(
transformResponseKeysToCamelCase,
transformErrorKeysToCamelCase
);
Note that keysToCamelCase
, serializeKeysToSnakeCase
are methods from our
open source pure utils library
@bigbinary/neeto-cist
.
While rolling out the change to all products, we wrote a JSCodeShift script to automatically add these flags to every Axios API requests in all Neeto products to ensure that nothing was broken due to it. Then the team had manually went through the code base and removed those flags while making the necessary changes to the code.
After the change was introduced the API code was much cleaner without the boilerplate for case conversion.
// Request
const createUser = ({ userName, fullName, dateOfBirth }) =>
axios.post("/api/v1/users", { userName, fullName dateOfBirth })
// Response
const { userName, fullName, dateOfBirth } = await axios.get("/api/v1/users/user-id-1");
In our work towards automating case conversion within neeto, we encountered several pain points.
During the rollout phase of our automated case conversion solution, there was an unavoidable requirement for manual intervention. As we transitioned existing code bases to incorporate the new mechanisms for automatic case conversion within Axios, each Axios call needed adjustment to remove the manual case conversion codes written before.
This stage demanded some manual work from our development teams. They updated and modified existing Axios requests across multiple projects to ensure they aligned with the new automated case conversion mechanism. While this manual effort temporarily increased workload, it was a necessary step to implement the automated solution effectively across neeto.
This phase highlighted the importance of a structured rollout plan and meticulous attention to detail. Despite the initial manual workload, once the changes were applied uniformly across the codebase, the benefits of automated case conversion quickly became evident, significantly reducing ongoing manual efforts and improving the overall efficiency of our development process.
As our initial implementation of automated case conversion, we used
keysToSnakeCase
method which recursively transformed all the keys to snake
case for a given object. It internally used transformObjectDeep
function to
recursively traverse through each key-value pair inside an object for
transformation.
import { camelToSnakeCase } from "@bigbinary/neeto-cist";
const transformObjectDeep = (object, keyValueTransformer) => {
if (Array.isArray(object)) {
return object.map(obj =>
transformObjectDeep(obj, keyValueTransformer, objectPreProcessor)
);
} else if (object === null || typeof object !== "object") {
return object;
}
return Object.fromEntries(
Object.entries(object).map(([key, value]) =>
keyValueTransformer(
key,
transformObjectDeep(value, keyValueTransformer, objectPreProcessor)
)
)
);
};
export const keysToSnakeCase = object =>
transformObjectDeep(object, (key, value) => [camelToSnakeCase(key), value]);
However, this recursive transformation approach led to a serialization issue,
especially with objects that required special treatment, such as dayjs
objects
representing dates. The method treated these objects like any other JavaScript
object, causing unexpected transformations and resulting in invalid payload data
in some cases.
To mitigate these serialization issues and prevent interference with specific
object types, we enhanced the transformObjectDeep
method to accommodate a
preprocessor function for objects before the transformation:
const transformObjectDeep = (
object,
keyValueTransformer,
objectPreProcessor = undefined
) => {
if (objectPreProcessor && typeof objectPreProcessor === "function") {
object = objectPreProcessor(object);
}
// Existing transformation logic
};
This modification allowed us to serialize objects before initiating the
transformation process. To facilitate this, we introduced a new method,
serializeKeysToSnakeCase
, incorporating the object preprocessor. For specific
object types requiring special serialization, such as dayjs
objects, we
leveraged the built-in toJSON
method, allowing the object to transform itself
to its desired format, such as a date string:
import { transformObjectDeep, camelToSnakeCase } from "@bigbinary/neeto-cist";
export const serializeKeysToSnakeCase = object =>
transformObjectDeep(
object,
(key, value) => [camelToSnakeCase(key), value],
object => (typeof object?.toJSON === "function" ? object.toJSON() : object)
);
This resolved the serialization issue for the request payloads. Since the response is always in JSON format, all values are objects, arrays, or primitives. It won't contain such 'magical' objects. So we need this logic only for request interceptors.
In simplifying our web development workflow at neeto, automating case conversion proved crucial. Despite challenges during implementation, refining our methods strengthened our system. By streamlining data translation and overcoming hurdles like serialization issues, we've improved efficiency and compatibility across our ecosystem.
If you're starting a new project, adopting automated case conversion mechanisms similar to what we've built in Axios can offer significant advantages. Implementing these standards from the beginning promotes consistency and simplifies how data moves between your frontend and backend systems. Introducing these practices early in your project's lifecycle helps sidestep the difficulties of adjusting existing code and establishes a unified convention throughout your project's structure.
For existing projects, adopting automated case conversion might initially come with a cost. Introducing these changes requires careful planning and execution to minimize disruptions. The rollout process might necessitate manual updates across various parts of the codebase, leading to increased workload and potential short-term setbacks.
If this blog was helpful, check out our full blog archive.