Converting Mirage JS IDs to numbers

Author Jesse Breneman Published on January 31, 2021

Mirage JS is a great library that makes mocking APIs super easy, allowing you to develop and test front end applications and not have to worry so much about the back end of your application. While using it to mock out an API, I ran across an interesting problem—IDs that are created by Mirage are strings while in the API I was mocking IDs are numbers. For the most part this was fine, but there were a few instances where the application I was mocking the API for actually relied on the IDs being numbers, so I wanted to figure out a way around this issue.

After searching around for solutions to this problem, I came across several ideas, and this method, which relies on a custom serialize function, was the only one that worked for me. There are examples out there on the web of how to do this, but the ones I found were a little over complicated and didn't work for me out of the gate.

The basics

To start, let's create a custom serialize function for our serializer that should already be set up in main.js. I'm using the RestSerializer that Mirage ships with, but any serializer should work. We're going to extend the serializer and add a serialize() method. The lines of code you see here in serialize() are straight from the Mirage docs, it's going to give us the response data as a raw javascript object or array that we can manipulate before sending it on.

js
import { createServer, RestSerializer } from 'miragejs';

createServer({
    serializers: {
        application: RestSerializer.extend({
            serialize() {
                const json = RestSerializer.prototype.serialize.apply(this, arguments);

                return json;
            },
        }),
    },
});

Now that we're got that set up, let's get into the actual data mapping. I'm going to split this into two functions, one that figures out if a key/value pair should be converted, and one that walks through the object and applies the changes recursively.

Let's look at figuring out if a key/value pair should be converted. Basically what we need to do is check if the key is named id and optionally look for other keys that end in Id. In my case, the api was in snake case, but I've changed it to camel case for this example since that's a little more normal. Additionally, we also need to make sure that we only convert strings, so we'll always return false if we're not checking a string. I've turned this into a utility function that we can use in our main mapping function.

js
const shouldConvert = (key, value) => {
    return (key === 'id' || key.endsWith('Id')) && typeof value === 'string';
};

We've taken care of figuring out whether or not a value should be converted, so let's create another function that we can use to actually do the converting for us. To start, let's handle the most complex case that we'll run into, objects, and then we're going to handle the other cases as well.

There are a bunch of different ways of checking for objects in JavaScript since almost everything is technically an object. For our purposes, if we just do a straight typeof data === 'object' check and then ignore any null values (typeof null === 'object' evaluates to true), we'll get what we're looking for, which is just a normal old object. Once we've made sure we're looking at an object, we loop though the object, check if values need to be converted and if so, convert them. If they don't need converted, we instead run the data through the function we're currently writing. This will ensure that we get nested arrays and objects, as we'll run them all through this function as well and convert those values as well.

js
const mapData = (data) => {
    if (typeof data === 'object' && data !== null) {
        return Object.entries(data).reduce((acc, [key, value]) => {
            const newValue = shouldConvert(key, value) ? +value : mapData(value);
            return {
                ...acc,
                [key]: newValue,
            };
        }, {});
    }
};

Now that we've gotten objects out of the way, let's handle the other types of data we might run into. For arrays, we want to just run the values in the array through our mapData function, to convert any IDs in objects that are in arrays. We also want a fallback for string values, so if data isn't an array or object, we want to just return the value as is.

js
const mapData = (data) => {
    if (Array.isArray(data)) {
        return data.map((item) => mapData(item));
    }

    if (typeof data === 'object' && data !== null) {
        return Object.entries(data).reduce((acc, [key, value]) => {
            const newValue = shouldConvert(key, value) ? +value : mapData(value);
            return {
                ...acc,
                [key]: newValue,
            };
        }, {});
    }

    return data;
};

And that's the data mapping done. From here, we just need to move this into main.js and run the data getting passed into our serialize method through the mapData we created and that's it, you should be getting ids that are actual numbers instead of strings.

js
import { createServer, RestSerializer } from 'miragejs';

const shouldConvert = (key, value) => {
    return (key === 'id' || key.endsWith('Id')) && typeof value === 'string';
};

const mapData = (data) => {
    if (Array.isArray(data)) {
        return data.map((item) => mapData(item));
    }

    if (typeof data === 'object' && data !== null) {
        return Object.entries(data).reduce((acc, [key, value]) => {
            const newValue = shouldConvert(key, value) ? +value : mapData(value);
            return {
                ...acc,
                [key]: newValue,
            };
        }, {});
    }

    return data;
};

createServer({
    serializers: {
        application: RestSerializer.extend({
            serialize() {
                const json = RestSerializer.prototype.serialize.apply(this, arguments);

                return mapData(json);
            },
        }),
    },
});

About the author