Reading Files

Let's leverage the Node.js ecosystem to read files in our project

Now, let's try reading a file! Here is how I did that and comments explaining the what and why:

import fs from 'fs';
import path from 'path';
import { Species } from '../types';

export const getSpecies = (id: number): Promise<Species> => {
  return new Promise((resolve, reject) => {
    
    // let's wrap this in a try/catch block, because we may be asked to read a 
    // file that isn't there!  But we can cleanly handle that, by letting the 
    // caller know that there was an error reading the file in our catch statement.
    try {
    
      // __dirname is a module constant that gets the current directory of 
      // our code.  We need to go up two directories to get to the root,
      // and then down the path of `/data/species/{id}.json`.  This `path`
      // package is the kosher way to create paths.  It can look a bit
      // much, but it's a safe way to access files and has a ton of superpowers.
      // more info is here: https://nodejs.org/api/path.html
      const filePath = path.join(__dirname, '..', '..', 'data', 'species', `${id}.json`);

      // here, we're reading the file at that path.  This may fail!  If the user
      // puts in a crazy random number or a string, or whatever, this will throw
      // an error.  You'll want to do some validation, and we'll get to that, 
      // so just know that calling this unprotected by validation isn't super safe
      // but since we're the only ones using our code, we can trust that it will
      // be okay for now.
      const data = fs.readFileSync(filePath);

      // let's console.log the data to see what we've got!
      console.log(data);

      resolve({} as Species)
    } catch (e) {
      reject("Pokemon not found")
    }
  })
}

In our src/index.ts, let's remove our "Hello world!" and put the following code:

import { getSpecies } from "./species-service/file";

getSpecies(7).then(console.log).catch(console.error);

When you call Promises in Javascript or Typescript, you normally pass in a function like

asyncFunctionYouCalled.then((x) => console.log(x))

What I did above was that, but even more syntactically awesome. console.log is a function, just as console.error is, and if we pass it in, it will call it, as it's practically the same as the example above.

If we run yarn start we should get something like this:

<Buffer 7b 0a 20 20 22 69 64 22 3a 20 37 2c 0a 20 20 22 6e 61 6d 65 22 3a 20 22 53 71 75 69 72 74 6c 65 22 2c 0a 20 20 22 74 79 70 65 73 22 3a 20 5b 0a 20 20 ... 136 more bytes>
{}

That isn't really useable to us. That first line is our console.log in our getSpecies function, and that {} is what we returned and so the .then() statement in our ./src/index.ts spat that out. Buffers are collections of binary data. There are a lot of different formats that files can come in. Postgres and MySQL store their data in files. Those files look a lot different than our files do, but since we know that they're written in JSON, we can parse them into JSON, and we can do that in one step.

Here is how we do that! In our ./src/species-service/file.ts finish off our file with the following code:

import fs from 'fs';
import path from 'path';
import { Species } from '../types';

export const getSpecies = (id: number): Promise<Species> => {
  return new Promise((resolve, reject) => {
    try {
      const filePath = path.join(__dirname, '..', '..', 'data', 'species', `${id}.json`);
      const data = fs.readFileSync(filePath);

      // first, we get the data in its string form. 
      // then we parse it using `JSON.parse()`
      // by saying our species is of type Species (species: Species)
      // Typescript will know that we're expecting it to be the specific
      // shape that we want to return our data in (as we're returning `Promise<Species>`
      const species: Species = JSON.parse(data.toString());
      
      // swap out our `{} as Species` and we have a functioning service!
      resolve(species)
    } catch (e) {
      reject("Pokemon not found") // [4]
    }
  })
}

Heck yeah! Let's call that now by running yarn start again! We should see this as our output!

{
  id: 7,
  name: 'Squirtle',
  types: [ 'Water' ],
  stats: {
    hp: 44,
    attack: 48,
    defense: 65,
    spAttack: 50,
    spDefense: 64,
    speed: 43
  }
}

BOOM.

Now, let's update our ./src/index.ts to include a file path that we don't support:

import { getSpecies } from "./species-service/file";

getSpecies(42).then(console.log).catch(console.error);

Running yarn start again should spit out the following error message:

Pokemon not found

Sweet! There is more that we'll be adding to our service, but that's a wrap for our basic Pokemon Service! Try tweaking it a bit, request different Species, or add a few more Species! Let me know what you do, and where you go with it.

In the next route in our Main Quest, we'll be adding a Business Logic Layer to use this data to create actual Pokemon that we can battle with.

Last updated