My first step to a full-stack Pokedex app

Using Typescript and Jest

My goal for this project

My goal at the end of all this is to have a full-stack Pokedex app. I could have used the data from the PokeAPI and built a frontend only, but then I wouldn't get to build my backend/serverless functions and interact with my database.

This full project is available on GitHub - https://github.com/BradPreston/getPokemonDataFromAPI

Getting the data

The first thing my project does is fetch the initial data.

const res = await axios.get("https://pokeapi.co/api/v2/pokemon?limit=151");
const pokemonResults: PokemonResult[] = await res.data.results;

Here, I'm using a custom type called PokemonResult that expects to have a name of type string and a URL of type string.

export type PokemonResult = {
  name: string
  url: string
}

I'm limiting my data to the first 151 for a couple reasons: 1) I'm only interested in the original Pokemon for the scope of this project, and 2) The speed of execution in the following functions is much higher with the limited set of data. Ultimately, it's just much easier to manage this limited set of data. Let's be honest though, the first 151 are the best Pokemon.

Grabbing the base data for each Pokemon

The next step in the project is to map over the pokemonResults and fetch the data for each Pokemon.

export default async function getPokemonData(url: string): Promise<PokemonData> {
  try {
    const res = await axios.get(url);
    const data = await res.data;

    return {
      pokedex_number: data.id,
      name: data.name,
      height: data.height,
      weight: data.weight,
      sprite: data.sprites.other["official-artwork"].front_default,
      types: data.types.map((t: Type) => t.type.name),
      description_url: data.species.url
    }
  } catch(err) {
    return err
  }
}

getPokemonData is passed the URL from the pokemonResults. For example, the first URL passed is to get the data for Bulbasaur - https://pokeapi.co/api/v2/pokemon/1/. I don't need all of the data from the response, so I only return the Pokedex number, name, height, weight, sprite image, pokemon type(s), and its description URL to fetch in the next step. This response fulfills my custom PokemonData type.

export type PokemonData = {
  pokedex_number: number
  name: string
  height: number
  weight: number
  sprite: string
  types: string[]
  description_url: string
}

Now to get the description of each Pokemon

It's time to use the description URL from the PokemonData type returned from the previous function.

export default async function getPokemonDescription(description_url: string): Promise<DescriptionData> {
  try {
    const res = await axios.get(description_url);
    const data = await res.data;

    // index finds the index of the flavor text with the version name red and language name en
    const index = res.data.flavor_text_entries.findIndex((entry: DescriptionEntry) => {
      return entry.version.name === "red" && entry.language.name === "en";
    });

    const description = data.flavor_text_entries[index].flavor_text
                        .replaceAll("\n", " ")
                        .replaceAll("\f", " ");

    return {
      id: data.id,
      color: data.color.name,
      shape: data.shape.name,
      description: description
    }
  } catch (err) {
    return err;
  }
}

getPokemonDescription fetches the data from the description URL returned above. Continuing our example with Bulbasaur, this URL would be - https://pokeapi.co/api/v2/pokemon-species/1/.

I only want the description from the first-generation games and only in English. I created a custom type the entry inside flavor_text_entries to verify the version name and language name. Then I return the index that fulfills that requirement.

Once I have the description, I want to replace all new lines and form feeds with one space. This allows me to style the text however I see fit later with little effort. This was a light change, but one that I felt was important.

After editing the description, I return the id, color, shape, and description which satisfies my custom DescriptionData type.

export type DescriptionData = {
  id: number
  color: string
  shape: string
  description: string
}

Settling the promises

I want to work with the data once it all comes back fulfilled, not one by one. Mainly, I want the data to be in proper order so I don't have to sort it later. Using Promise.allSettled allowed me to use the data once it's all been fulfilled.

export default async function settlePokemonData(results: PokemonResult[]): Promise<PromiseSettledResult<Pokemon>[]> {
  try {
    const pokemon = await Promise.allSettled(results.map(async (pkmn: PokemonResult) => {
      const pokemonData = await getPokemonData(pkmn.url);
      const pokemonDescription = await getPokemonDescription(pokemonData.description_url);

      const result: Pokemon = {
        name: pokemonData.name,
        pokedex_number: pokemonData.pokedex_number,
        height: convertToInches(pokemonData.height),
        weight: convertToPounds(pokemonData.weight),
        sprite: pokemonData.sprite,
        shape: pokemonDescription.shape,
        color: pokemonDescription.color,
        types: pokemonData.types,
        description: pokemonDescription.description
      }
      return result;
    }));

    return pokemon
  } catch(err) {
    return err
  }
}

The settlePokemonData function is where I map over the PokemonResults from the beginning of the blog. You can see inside the map where I call the getPokemonData and the getPokemonDescription as well. Once the data is settled from each function, I return the values that satisfy my custom Pokemon type.

I created a function for convertToInches and convertToPounds because the data didn't come back in a usable format for pounds or inches.

export default function convertToInches(height: number): number {
  // Pokeapi sets meters without decimal places, so I convert to a 1 place decimal number by dividing by 10.
  // This gives me results in meters that are accurate to the official Pokedex.
  const inches = (height / 10) * 39.37;
  return parseFloat(Math.round(inches).toFixed(1));
}

export default function convertToPounds(weight: number): number {
  // Pokeapi sets kilograms without decimal places, so I convert to a 1 place decimal number by dividing by 10.
  // This gives me results in kilograms that are accurate to the official Pokedex.
  const pounds = (weight / 10) * 2.205;
  return parseFloat(pounds.toFixed(1));
}

The height and weight values returned from the PokeAPI weren't exactly in meters or kilograms. However, if you add a decimal place one value to the left from the end of the number, that was almost exactly on for meters and kilograms according to the official Pokedex in Pokemon Red/Blue/Yellow. I divided the height and weight each by 10 to give me that decimal place and then did the kilograms to pounds and meters to inches conversions. I only wanted one decimal place in the end, so I fixed it to one using toFixed and then parsed the result as a float.

Generating a JSON file with my data

Now that I have the exact data I want and none of the data I don't, I can create a JSON file to store the info. This JSON data will be used later to populate MongoDB (or whatever NoSQL database I use for this specific project).

export default function createJSONFile(pkmn: PromiseSettledResult<Pokemon>[], filename: string) {
  pkmn.forEach(({value}: PromiseFulfilledResult<Pokemon>) => {
    const file = fs.readFileSync(filename);

    if (file.length === 0) {
      // if the file is empty, write to the file first
      fs.writeFileSync(filename, JSON.stringify([value]));
    } else {
      // otherwise, parse the data and append to it. Then write back into the file
      const json = JSON.parse(file.toString());
      json.push(value);
      fs.writeFileSync(filename, JSON.stringify(json));
    }
  });
}

createJSONFile loops over the array of Pokemon returned from the last steps and passes the data into a specified filename. In the loop, I first read the data in the file. If the file is empty, then I write the Pokemon data to the JSON file. If the file isn't empty, then I parse the data and push the value to the parsed object. Once the data is appended to the object, I then stringify it and re-write the JSON file.

Where to next?

The next steps are to create the database, populate the database, build the functions to get the data from the database, etc. Essentially, this part of the project was only to get the data in a usable state for what I needed.

The full project is hosted on GitHub - https://github.com/BradPreston/getPokemonDataFromAPI. You'll find tests in the src folder as well. I might make a blog post regarding the tests for this project, but really, the data is well-tested from PokeAPI, so I don't need to do extensive testing.