Source Allies Logo

Sharing Our Passion for Technology

& Continuous Learning

<   Back to Blog

Some fun JavaScript features for API functionality

fun-loving javascript monkeys

Some fun JavaScript features for API functionality

The more I work in JavaScript, the better I recognize how certain patterns fit into the intent and functionality of a program. Reading and writing larger sections of code becomes easier as I'm able to "chunk" the functionality into brain-sized pieces. While this post is mostly about some of my favorite JavaScript features, I hope it also gives the reader some ideas and inspiration for developing their personal style, be it to write more concise, functional, or maintainable code.

Scenario: Zoo API

Consider a scenario where our partner is a zoo that receives daily suggestions for what foods animals should be fed. (I’m using TypeScript here to define Animal and Food for clarity, but it’s not essential for the rest of the article). The zoo’s data might look like this:

enum Species {
  TIGER,
  BEAR,
  POSSUM,
}

type Animal = {
  name: string;
  species: Species;
  cageNumber: number;
};

enum Stock {
  IN_STOCK,
  OUT_OF_STOCK,
}

const lioness: Animal = {
  name: "Lioness",
  cageNumber: 1,
  species: Species.TIGER,
};

const zooAnimals: Animal[] = [lioness];

And they use an external API that provides data with the following structure:

type Food = {
  name: string;
  fatContent: number;
  live: boolean;
};

type SuggestedAnimalDiets = {
  species: Species;
  foods: Food[];
}[];

const mouse: Food = {
  id: "mouse",
  fatContent: 0.12,
  live: true,
};

const suggestedAnimalDiets: SuggestedAnimalDiets = [
  {
    species: Species.TIGER,
    foods: [mouse],
  },
];

Requirements

The zoo wants you to deliver the following:

  1. Verify that there's a suggested diet for every animal at the zoo
  2. Generate a list of food that needs to be stocked for each animal
  3. Generate a list of food that needs to be stocked across the entire zoo

1. Validating Input Requirement

With multiple data sources, it's useful to create maps that help find relevant input. Given the example structure for our external API, we can use an accumulator to quickly create a map with animal species as a key.

Creating a lookup table with reduce

Accumulators take an initialization value as the second argument (in this case, {}), and with each iteration, they operate on this object. The standard is to define the result of the accumulation as "previous" and the current object in the iteration as "current." We apply the spread operator to previous, effectively unrolling the existing map at each iteration to align with the current key and value.

const animalDiets: Record<string, Food[]> = suggestedAnimalDiets.reduce(
  (previous, current) => ({
    ...previous,
    [current.species]: current.foods,
  }),
  {}
);

Using every for validation

We now have a map from the external API with Animal.species as the key and Animal.foods as the value. We can use every to iterate through a list of zooAnimals and check that there's a suggested diet for each.

const haveDietForEveryAnimal = zooAnimals.every(
  (animal) => animalDiets[animal.species]
);

Here, the callback inside every will return false for any falsy value(false, 0, "", undefined, null, NaN). Since accessing a map with a non-existent key yields undefined, this approach satisfies the requirement.

The approach to the solution is intuitive, efficient, and generates a reusable map for animal species.

2. Determining stock scoped to animal

Next, we have a more complicated criteria that applies to every ZooAnimal. We aim to compile a list that includes each animal, their cage number, and the food that needs to be restocked. Assuming we can query the stock status by food item, we will utilize a basic query wrapper for illustration:

const foodNotInStock = (food: Food) =>
  queryFoodStockStatus(food) !== Stock.IN_STOCK;

Point-free calls

First, we start with a map and use some functional programming by passing a function as an argument to the map. A point-free definition, like the one below, passes a tacit function, meaning its arguments are included in the function call. Although we're not passing animal in explicitly, JavaScript offers us this nice feature to simplify the experience of passing in the iterator's implicit argument automatically. Consequently, each iteration of Animal is passed to the provided function. While we could write:

const allStockNeeded = zooAnimals.map((animal) => stockNeededForAnimal(animal));

A point-free call is more friendly:

const allStockNeeded = zooAnimals.map(stockNeededForAnimal);

Then we define the function stockNeededForAnimal. This function requires access to the species-diet map we previously created, so it should be defined as an internal function.

Breaking down a two-dimensional array

We encounter another array when defining the food items for the animal's diet. This time, we'll utilize a tacit function within a filter method. Recall that filter retains elements in the array for which the provided expression evaluates to true. Since we want to return the food items which are not in stock for an animal, we incorporate a negation in the function name.

const stockNeededForAnimal = (animal: Animal) => ({
  name: animal.name,
  cageNumber: animal.cageNumber,
  foodsNeeded: animalDiets[animal.species].filter(foodNotInStock),
});

foodNotInStock function could easily be modified to incorporate additional complexity without affecting the structure of the other functions.

In the above code, we've implemented two point-free calls within a map and a filter to manage logic over a two-dimensional array. While the entire function could have been written using anonymous functions, defining point-free definitions clarifies the purpose of each layer of logic, potentially enhancing readability and maintainability.

For comparison, here's what the code would look like with anonymous functions:

const allStockNeeded = zooAnimals.map((animal) => ({
  name: animal.name,
  cageNumber: animal.cageNumber,
  foodsNeeded: animalDiets[animal.species].filter(
    (food) => queryFoodStockStatus(food) !== Stock.IN_STOCK
  ),
}));

Here's how it would look all together with the helper functions:

const allStockNeeded = zooAnimals.map(stockNeededForAnimal);

const stockNeededForAnimal = (animal: Animal) => ({
  name: animal.name,
  cageNumber: animal.cageNumber,
  foodsNeeded: animalDiets[animal.species].filter(foodNotInStock),
});

const foodNotInStock = (food: Food) =>
  queryFoodStockStatus(food) !== Stock.IN_STOCK;

Granted, this is largely a matter of preference, and the example here is for illustration only! Breaking down complexity into separate functions can sometimes enhance both the readability and maintainability of the code. The transition points inside of map and filter create nice opportunities to define a specific purpose for a fresh block of code.

Destructuring and property shorthand

I want to cover destructuring in this section, too, because it's such a fun and convenient feature. Suppose I need to validate the animal's name and cage number before checking the stock of the food. I'd be writing animal.name and animal.cageNumber at least twice: once to pass them to the validation function and again to define the return object. I could define each variable separately from the object:

const name = animal.name;
const cageNumber = animal.cageNumber;

But, when extracting properties from an object, you can use the property names as variable names through destructuring. By defining the variables inside curly braces {}, JavaScript assigns the values of the properties on animal to variables with matching names. Additionally, these variable names can be reassigned. For example, I can reassign name to ensure it clearly applies to the specific animal.

const stockNeededForAnimal = (animal: Animal) => {
  const { name: animalName, cageNumber, species } = animal;
  validateAnimal(animalName, cageNumber);
  return {
    name: animalName,
    cageNumber,
    foodsNeeded: animalDiets[species].filter(foodNotInStock),
  };
};

You can also use destructuring on the implicit arguments of functions like map and filter, as I'll demonstrate in the next section.

Lastly, property shorthand is a slick and easy JavaScript trick. Similar to destructuring, it works on the principle that the variable names align with a property names. So you could assign the variable to the property like so:

return
    {
        name: name,
        cageNumber: cageNumber,
        foodsNeeded: foodsNeeded,
    }

But it's more fun to use shorthand and let JavaScript do it for you:

return
    {
        name,
        cageNumber,
        foodsNeeded,
    }

3. Determining stock scoped to zoo

Deduplication using flatMap with Set

What if we didn’t need to specify which animal required the food stock? We could generate a comprehensive list of all food items needed across all species in the zoo. Subsequently, we could query the stock status for each item in this list. (Note the destructuring of species from the implicit animal object.)

const uniqueFoodsForDiets = zooAnimals.flatMap(
  ({ species }) => animalDiets[species]
);

In this implementation, flat map simplifies the structure of the result by concatenating each subarray into a single array. However, this may result in duplicate food items if multiple animals require the same food (say both Lioness and Sir Tiger eat mice). One straightforward and efficient way to remove duplicates is to create a new Set. By passing the mapped array directly to the Set constructor, we can efficiently define the new set on the fly.

const uniqueFoodsForDiets = new Set(
  zooAnimals.flatMap({ species } => animalDiets[species])
)

Now the set must be turned back into an array so it can be filtered based on stock status.

const uniqueFoodsForDiets = new Set(
  zooAnimals.flatMap({ species } => animalDiets[species])
);
const foodsNotInStock = Array.from(uniqueFoodsForDiets).filter(foodNotInStock);

In this post, we've explored examples of several JavaScript features that enhance the code-writing experience, making it feel more direct and approachable.

  • Accumulators
  • "every"
  • Point-free functions
  • Destructuring
  • Property shorthand
  • "flatMap"
  • "Set"

I hope these concepts help you write and read code more confidently, abundantly, and enjoyably. Happy JavaScripting!