Using Normalizr

27. April, 2022 5 min read Develop

Normalizing API responses

Normalization is required when interacting with multiple API endpoints that have references.

We are using normalizr for that particular job at Divio. Tools such as React Query or rtk-query are great alternatives. Today we would probably use React Query. Yet, the migration effort to switch outweighs the benefits, for now. ¯\_(ツ)_/¯

So, what is the problem?

Lets have a look at the following two API responses:

// Categories API response
const categoriesApi = [
  { uuid: 'iu6h3wr74vc2bhugglgzmp3hhu', name: 'Movie' },
  { uuid: '4g35twfl3vhu3njuxgbtardjrm', name: 'TV Show' },
  { uuid: 'oiz6zif455h3hctzzhobak5l2y', name: 'Other' },
];

// Movies API response
const moviesApi = [
  {
    uuid: '3l5buhzt7rcbdmhntsrdlxd3h4',
    name: 'Memento',
    category: 'iu6h3wr74vc2bhugglgzmp3hhu',
  },
  {
    uuid: 'zxevaw7jkfbhdiflgfp5lxwrku',
    name: 'Pulp Fiction',
    category: 'iu6h3wr74vc2bhugglgzmp3hhu',
  },
  {
    uuid: 'uexridtrxnb3tls2az7x3eemvu',
    name: 'Spirited Away',
    category: 'oiz6zif455h3hctzzhobak5l2y',
  },
  {
    uuid: 'x3eemv2az7x3etsrdlxd3h4dda',
    name: 'Breaking Bad',
    category: '4g35twfl3vhu3njuxgbtardjrm',
  },
];
  • The categoriesApi provides a list of categories. It uses the uuid to mark each entry with a universally unique identifier.
  • The moviesApi follows a similar pattern but holds a reference to the category uuid. This approach allows us to separate the data and avoid repeating unnecessary information, making the API slim and fast.

This response is not what we desire when passing the data to a component. We would want to have the category inlined inside the movie like so:

{
  "uuid": "3l5buhzt7rcbdmhntsrdlxd3h4",
  "name": "Breaking Bad",
  "category": {
    "uuid": "4g35twfl3vhu3njuxgbtardjrm",
    "name": "TV Show",
  }
}

This step is where normalizr comes into play. Normalizr takes the API response and stores them as entities internally, and links them together through a schema. Let’s have a look at how that would work.

Setting everything up

We will be using our two API responses categoriesApi and moviesApi, as references moving forward. Now, create a JavaScript project through npm init and install normalizr through npm install normalizr --save.

We are going to import normalize, denormalize an schema from normalizr. Add the following line on the top:

import { normalize, denormalize, schema } from 'normalizr';

Next, we need to let normalizr know about our data structure. This is where our entities will be stored and how normalizr knows what objects are linked. To define our entities, we call schema.Entity:

const category = new schema.Entity(
  // here, we define the name of the entity
  'categories',
  {},
  // the idAttribute is used to identify references
  { idAttribute: 'uuid' }
);
const movie = new schema.Entity(
  'movies',
  {
    // here, we let normalizr know that movies
    // has a reference to another category
    category: category,
  },
  { idAttribute: 'uuid' }
);

These definitions allow us to normalize the data from our API endpoints when calling normalize(). Normalizr requires a schema that defines how the data should look like this:

const dataSchema = {
  categories: [category],
  movies: [movie],
};

Now, we can normalize the data by using normalize():

const normalized = normalize(
  {
    categories: categoriesApi,
    movies: moviesApi,
  },
  dataSchema
);

The output of normalized should look something like:

{
  "entities": {
    "categories": {
      "iu6h3wr74vc2bhugglgzmp3hhu": {
        "uuid": "iu6h3wr74vc2bhugglgzmp3hhu",
        "name": "Movie",
      },
      ...
    },
    "movies": {
      "3l5buhzt7rcbdmhntsrdlxd3h4": {
        "uuid": "3l5buhzt7rcbdmhntsrdlxd3h4",
        "name": "Breaking Bad",
        "category": "4g35twfl3vhu3njuxgbtardjrm"
      }
    }
  },
  ...
}

We do not have the data in our desired format yet. To achieve this, we need to denormalize the data. This step is where the entities will be linked together in a more usable format. Let’s denormalize() our data:

const denormalized = denormalize(
  {
    categories: normalized.result.categories,
    movies: normalized.result.movies,
  },
  dataSchema,
  normalized.entities
);

Denormalizing is finally bringing the data together in a format where we can process the data:

{
  "categories": [
    {
      "uuid": "iu6h3wr74vc2bhugglgzmp3hhu",
      "name": "Movie",
    },
    ...
  ],
  "movies": [
    {
      "uuid": "3l5buhzt7rcbdmhntsrdlxd3h4",
      "name": "Memento",
      "category": {
        "uuid": "iu6h3wr74vc2bhugglgzmp3hhu",
        "name": "Movie",
      }
    },
    ...
  ]
}

We could also remove categories: [category] from the dataSchema if we do not care about them individually. The categories will be inlined inside the movie object itself. However, that depends on how you process your data.

Finally, this is how everything in the file should look like:

import { normalize, denormalize, schema } from 'normalizr';

const categoriesApi = [
  { uuid: 'iu6h3wr74vc2bhugglgzmp3hhu', name: 'Movie' },
  { uuid: '4g35twfl3vhu3njuxgbtardjrm', name: 'TV Show' },
  { uuid: 'oiz6zif455h3hctzzhobak5l2y', name: 'Other' },
];

const moviesApi = [
  {
    uuid: '3l5buhzt7rcbdmhntsrdlxd3h4',
    name: 'Memento',
    category: 'iu6h3wr74vc2bhugglgzmp3hhu',
  },
  {
    uuid: 'zxevaw7jkfbhdiflgfp5lxwrku',
    name: 'Pulp Fiction',
    category: 'iu6h3wr74vc2bhugglgzmp3hhu',
  },
  {
    uuid: 'uexridtrxnb3tls2az7x3eemvu',
    name: 'Spirited Away',
    category: 'oiz6zif455h3hctzzhobak5l2y',
  },
  {
    uuid: 'x3eemv2az7x3etsrdlxd3h4dda',
    name: 'Breaking Bad',
    category: '4g35twfl3vhu3njuxgbtardjrm',
  },
];

const category = new schema.Entity('categories', {}, { idAttribute: 'uuid' });
const movie = new schema.Entity(
  'movies',
  {
    category: category,
  },
  { idAttribute: 'uuid' }
);

const dataSchema = {
  movies: [movie],
};

const normalized = normalize(
  {
    movies: moviesApi,
  },
  dataSchema
);

console.log('entity store', normalized);

const denormalized = denormalize(
  {
    categories: normalized.result.categories,
    movies: normalized.result.movies,
  },
  dataSchema,
  normalized.entities
);

console.log('denormalized', denormalized);

This short tutorial should explain the basics of normalization when using different API endpoints that reference each other. Just be sure to avoid circular references 🤯.

‘Till next time!