So far, you've learned two ways to write a function:
(...) => {...}
(the "fat arrow" method): Taught in JS0function(...) {...}
First introduced in this chapter for writing prototype
functionsWhat is the difference between the two function declarations?
function() {....}
vs () => {....}
?
The difference has to do with the this
keyword.
A long time ago, there were no fat arrow functions. You had to write functions like this:
const fun = function(a,b) {...}
;[5, 2, 3].push(10)// Inside the function `push`, **this** refers to whatever// comes before the `.` ([5,2,3]) in this case.const a = [9, 8, 7]a.getLast()// inside the function `getLast`, **this** refers to// whatever comes before the `.` (a) in this case.alert('Hi there')// What about functions without `.`?// What would **this** be in the function `alert`?// Answer:// If there is no `.`,// the browser will automatically put `window` when it runs your code:// window.alert("hello")// Therefore, inside the alert function, **this** refers to the window object// In nodejs, nodejs will automatically put `global` when it runs your code:// global.alert("hello")// Therefore, inside the alert function, **this** refers to the global object
What about the following example?
setTimeout(function () {console.log(this) // What is this? Answer below})
In functions like setTimeout
that were written by someone else, you can't
always know what this
is, because setTimeout
could be doing something like
the following:
const setTimeout = function (fun) {const newObject = {}newObject.fun = fun // now newObject has the input functionnewObject.fun()// in the function passed into `setTimeout`,// `this` refers to newObject}
This makes many prototype functions really difficult to write because this
will never refer to the object or array.
const arr = [1, 2, 3]Array.prototype.delayedLast = function () {setTimeout(function () {console.log(this[this.length - 1])}, 1000)}arr.delayedLast() // after 1000ms,// The browser not print out what we expect,// because `this` does not refer to the array [1,2,3].
The solution is to use a variable to store the value of this
before anything
else can change it, then use that variable in the argument function passed into
setTimeout
.
const arr = [1, 2, 3]Array.prototype.delayedLast = function () {const self = thissetTimeout(function () {console.log(self[self.length - 1])}, 1000)}arr.delayedLast() // after 1000ms,// The browser will print out 3. It works!
What happens when we run this code?
function Person(age) {this.age = age}Person.complain = function () {if (this.age >= 100)return "I'm getting way too old for these JavaScript exercises"return "I'm not old enough"}const yoda = new Person(419)console.log(yoda.complain())
Throws an error (undefined in not a function) because the complain function is
on Person
, not Person.prototype
.
When we try to access yoda.complain
, there's no property on the yoda
object
called complain complain
property, so it checks Person.prototype's prototype
which is Object.prototype
and there's no complain
property on
Object.prototype
and there are no more prototypes to follow in the prototype
chain so yoda.complain
is undefined
and we can't call undefined
like it's
a function so we get a runtime error.
We could still technically call the complain function like this to pass in
yoda
as the this
argument: Person.complain.call(yoda)
, but that would be
akward. The better way to fix this is to add complain
to Person.prototype
like Person.prototype.complain = function() {}
.
What happens when we run this code?
function Person(age) {this.age = age}Person.prototype.complain = () => {if (this.age >= 100)return "I'm getting way too old for these JavaScript exercises"return "I'm not old enough"}const yoda = new Person(419)console.log(yoda.complain())
Silent logic error. When we define Person.prototype.complain, we set it as an
arrow function. Arrow functions bind the current this
value in scope to that
arrow function so it's functionally similar to defining a function and binding
it, so this () => { ... }
is functionally equivalent to this
(function() { ... }).bind(this)
.
Using an arrow function and binding the this
value is a problem because later
when we try to pass in yoda
as the this
argument by passing yoda
to the
left of the dot in yoda.complain()
, the this
value passed in will be the
value that was bound when the function was defined. Since we're in the global
scope, the this
value is window
or global
.
So we'll get a silent logic error because we'll check this.age, which is
window.age or global.age which is undefined (unless there's a global variable
with the name age) and undefined >= 100 is false. So the complain function will
always return "I'm not old enough" even if the person is really old. The way to
fix this is by defining Person.prototype.complain as an unbound non-arrow
function like this: Person.prototype.complain = function()
.
The Fat Arrow () => {}
was designed to solve the above! Using a fat arrow
function, you can write functions that use this
to refer to the affected
object without creating a new variable.
const arr = [1, 2, 3]Array.prototype.delayedLast = function () {setTimeout(() => {console.log(this[this.length - 1])}, 1000)}arr.delayedLast() // After 1000ms,// the browser will print out 3. It works!
Some JavaScript engineers would tell their engineers to always use semicolons at the end of each statement. Otherwise, the code may all get mixed together. If we rewrite the above example with the following code, it will not run
Array.prototype.delayedLast = function () {setTimeout(() => {console.log(this[this.length - 1])}, 1000)}[(1, 2, 3)].delayedLast()
This is because the computer reads the code like this:
...}[1,3,3].delayedLast()
, which means something else (The author himself
doesn't know what it means, so this level of detail is probably not important to
learn right now).
The solution is to put ;
between the previous code and the array to separate
it.
Array.prototype.delayedLast = function () {setTimeout(() => {console.log(this[this.length - 1])}, 1000)};[1, 2, 3].delayedLast()
The Fat Arrow is a lighter way to write functions because unlike the old way of
writing function
, fat arrow functions do not create a this
variable. Looking
at the setTimeout
code above:
...setTimeout( () => {console.log(this[this.length-1])}, 1000)...
The function argument that we pass into setTimeout
is a fat arrow function and
therefore does not have its own this
variable. As a result, when the argument
function runs, this
simply refers to the this
value in the outer function,
which is the object.
It is common for software engineers to use fat arrows when writing functions
() => {...}
. If you need to use this
in your function, like in the
delayedLast function
above, you must write functions in the old fashioned
way: function () {...}
This way you can use this
to refer to the array to
the left of delayedLast
. Any additional functions that you write inside
(such as the function argument to setTimeout
) should be fat arrow functions
so they have access to the this
variable.
Imagine you have created a bunch of awesome functions that you want to use in
other files. To do this, you simply give the exports
key of the module
object a value. Whatever value you set will be exported. module.exports
is a
keyword provided by nodeJS. You've seen it used to export solution files to a
test file.
Some modules are dropping support for commonJS. You may have to use import
instead of require
and write "type": "module"
into your package.json
file. Read more
ESM modules
module.exports = [1, 2, 3] // What is module.exports?// What type of data is module?
module.exports = [1, 2, 3] // module.exports is an array: [1,2,3]// module is obviously an object because it has a key called exports.
Export: Any file that exports something can be called a library. Here we'll create two simple libraries.
Create a file called helper1.js
and write in the code below.
./helper1.js
- exports an object with 2 keys: data
(object) and
getData
(function)
const info = {ironman: 'arrogant',spiderman: 'naive',hulk: 'strong'}module.exports = {data: info,getData: key => {return info[key]}}
Create a file called helper2.js
and write in the code below.
./helper2.js
- exports an array
module.exports = ['ironman', 'strange', 'thor', 'spiderman', 'hulk']
Import: We will create a file that imports the libraries created in the steps above.
Create a file called solution.js
and write in the code below.
./solution.js
- node will be running this file. helper1.js
and
helper2.js
are files from the section above.
// myObj takes the value of module.exports, which is an objectconst myObj = require('./helper1.js')// myArr takes the value of module.exports, which is an arrayconst myArr = require('./helper2.js')const result = myArr.filter(e => {return myObj.getData(e)}) // what is result?console.log(result)
// myObj takes the value of module.exports, which is an objectconst myObj = require('./helper1.js')// myArr is ["ironman", "strange", "thor", "spiderman", "hulk"]const myArr = require('./helper2.js')const result = myArr.filter(e => {return myObj.getData(e)})// result is ["ironman", "spiderman", "hulk"]/*When filter runs....e is "ironman", myObj.getData("ironman") returns "arrogant" (truthy)e is "strange", myObj.getData("strange") returns undefined (falsey)e is "thor", myObj.getData("thor") returns undefined (falsey)e is "spiderman", myObj.getData("spiderman") returns "naive" (truthy)e is "hulk", myObj.getData("hulk") returns "strong" (truthy)filter returns ["ironman", "spiderman", "hulk"]*/
./myObj.js
, ./myFun.js
, that export an object and a
function respectively.// myObj.jsmodule.exports = {name: 'Butterfree',level: 11}// myFun.jsmodule.exports = () => {return 500}
console.log
your output to
make sure your functions are called correctly.// new file (any name, same folder as myObj.js and myFun.js)const pokemon = require('./myObj')const fun = require('./myFun')const sum = fun() + pokemon.levelconsole.log(sum)
Now that you know how to use require
, let's try to require a library that
other people have written. A simple library that does not require any download
is fs
, a library that gives you functions to interact with the files and
folders on your computer.
const fs = require('fs')fs.readdir('./', (err, files) => {console.log(files) // files should be an array})
Exercise: Edit the code so you only console.log filenames with length < 10.
const fs = require('fs')fs.readdir('./', (err, files) => {files.forEach(fileName => {if (fileName.length < 10) {console.log(fileName)}})})
fs.readdir
takes in 2 arguments, a string and a function.
Why do you need to pass in a function?
Just like how it takes you time to read a book, fs.readdir
needs time to look
through the entire folder. When it is done looking through the folder, it will
run your function and pass in 2 arguments: error (if there are issues, null
otherwise), and an array of filenames in the folder.
fs.readdir('./', (err, files) => {console.log('a')})console.log('b')// What gets printed out?
// "b"// "a"// This is because it takes time for fs.readddir to read the folder.
Now that you know how to import files, you should try to import some functions that other people have written!
Write a function called makeFiles that takes in a number (X) and creates X+1
files using fs.writeFile
.
The filenames should look like: trainer0.txt
, trainer1.txt
,
trainer2.txt
, ... trainerX.txt
.
When you open the file, each file should have the contents:
Gotta catch 'em all
fs.writeFile
takes in 3 arguments:
Here's an example of how fs.writeFile
works:
fs.writeFile('./today.txt', 'today is a beautiful day', () => {})// This will create a file called today.txt in the same folder// When you open the file, it will say 'today is a beautiful day'
const fs = require('fs')describe('makeFiles function', () => {fn.makeFiles(2)const files = fs.readdirSync('./')it('should create 3 files', () => {const foundAll =files.find(e => {return e === 'trainer0.txt'}) &&files.find(e => {return e === 'trainer1.txt'}) &&files.find(e => {return e === 'trainer2.txt'})expect(foundAll).toBeTruthy()})it('should put "Gotta catch \'em all" in the files', async () => {await fs.readFile('./trainer1.txt', { encoding: 'utf8' }, (_err, data) => {expect(data).toEqual("Gotta catch 'em all")})})})
const fs = require('fs')const makeFiles = (n, i = 0) => {if (i > n) {return}fs.writeFile(`./trainer${i}.txt`, "Gotta catch 'em all", () => {})return makeFiles(n, i + 1)}
Write a function called listFiles that reads the current folder and creates a
file called files.html
that looks like the following:
<h1>file1</h1><h1>file2</h1>...<h1>fileX</h1>
We won't be able to test this one since we don't know all the files in your folder.
const fs = require('fs')const listFiles = () => {fs.readdir('./', (err, files) => {const str = files.reduce((acc, f) => {return `${acc}<h1>${f}</h1>`}, '')fs.writeFile('./files.html', str, () => {})})}
You might have tried putting all the <h1>
s into an array and then writing it.
Unfortunately, fs.writeFile
writes arrays with commas separating the elements,
so reduce is a better choice here.
An API (application programming interface) is an interface that other engineers set up for you to interact with. For example:
... and many, many more.
What is an example of something you can build with the APIs mentioned above?
You can write a file that sends a text message to each of your friends every week with the top-rated movies on Netflix!
// This code does not work.// It is meant to give you an idea of what the code// would look like.// Take the time to read and understand the code.const request = require('request')const txtTopRatedMoviesTo = people => {request('netflix.com/api/topCatalog', movies => {people.forEach(number => {// A Twilio token is needed so twilio knows it's me// and charges me for every message I sendrequest('twilio.com/api/sendMessage', { token: twilio_token, to: number })})})}const runApp = () => {// A Google token is needed so Google knows it's merequest('google.com/contacts', { token: google_token }, contacts => {txtTopRatedMoviesTo(contacts)})setTimeout(runApp, 7 * 24 * 60 * 60 * 1000)}runApp()
The above code is good, readable code with good function names.
The below example, with multiple nested request
function calls, is a form of
bad code called callback hell. We'll learn a little more about this later.
const request = require('request')const runApp = () => {// A Google token is needed so Google knows it's merequest('google.com/contacts', { token: google_token }, contacts => {request('netflix.com/api/topCatalog', movies => {people.forEach(number => {// A Twilio token is needed so Twilio knows it's me// and charges me for every message I sendrequest('twilio.com/api/sendMessage', {token: twilio_token,to: number})})})})setTimeout(runApp, 7 * 24 * 60 * 60 * 1000)}runApp()
There are many libraries we can use to send a request, and the request
library
is one of them. You might have noticed it in the above examples. You can use
this library to send requests to APIs and even to retrieve web pages like your
browser does.
To run the below code, you'll need to install request by running
npm install request
.
const request = require('request')// We are sending a request to a Pokemon API to get Pokemon data.// This one actually works!request('https://pokeapi.co/api/v2/pokemon/?offset=20&limit=20',(err, res, data) => {console.log(data) // data is JSON// We use JSON.parse to parse the JSON back into an objectconst pokeInfo = JSON.parse(data)console.log(pokeInfo.results) // Array of pokemon!})
A typical URL looks like the above.
&
separates the different pieces
of data.https://macys.com/shoes?size=4&brand=allbirds&type=outdoors
When you type a URL into your browser, the browser sends a request just like the Pokemon example above. In fact, if you send a request to any website, such as https://news.ycombinator.com, you will see the same HTML response that the browser gets back when it sends the request. The browser then reads the HTML and follows the instructions defined by each tag.
const request = require('request')request('https://news.ycombinator.com', (err, res, data) => {console.log(data) // data is a string of HTML tags})
A note about APIs Different APIs/companies will respond with data in
different formats. Register.gov, Facebook.com, google.com, and c0d3.com all
have different data structures. They may respond back with an array of
objects, an object of objects, an array of strings, etc. So you gotta try out
each API before coding your project! Usually software engineers try out APIs
by using PostMan or by writing requests into a
JavaScript file and running the file with node
. Due to possible security
implications, software engineers normally will not try out APIs in the
browser.
[https://www.c0d3.com/api/lessons](https://www.c0d3.com/api/lessons)
and
console.log all the titles.Don’t forget that if you want to do anything interesting with the results of a
request you’ll usually have to JSON.parse
it.
First, open the url in your browser to understand the data. If the data looks
messy, installing a browser extension like
JSON View would
help. Looking at the data, we will have 10 titles, so if our function worked out
console.log
should be called 10 times.
Tests
We are redefining console.log
to overwrite its original functionality
(Mocking)
Since there are 10 lessons, after we run getLessons
, our custom
console.log
should have run 10 times.
Therefore, we keep a variable i
to keep track of how many times the
function has been called.
getLesson
takes time to run, so we will give it 2 seconds to run before
expecting i
value to be 10.
jest.mock('request')const request = require('request')const lessons = require('./printLessonTitles')describe('lessons', () => {test(`console log should not be called if lessons `, () => {request.mockClear()lessons.printLessons()expect(request.mock.calls.length).toEqual(1)const firstCall = request.mock.calls[0]expect(firstCall[0]).toEqual('https://c0d3.com/api/lessons')})test('console.log should be called once if length of lessons array is 1', () => {request.mockClear()lessons.printLessons()console.log = jest.fn()request.mock.calls[0][1]({},{},JSON.stringify([{title: 'testing'}]))expect(request.mock.calls.length).toEqual(1)expect(console.log.mock.calls[0][0]).toEqual('testing')})test('console.log should return 3 times if lessons array has 3 elements', () => {request.mockClear()lessons.printLessons()console.log = jest.fn()request.mock.calls[0][1]({},{},JSON.stringify([{title: 'Testing1'},{title: 'Testing2'},{title: 'Testing3'}]))expect(console.log.mock.calls.length).toEqual(3)expect(console.log.mock.calls[0][0]).toEqual('Testing1')expect(console.log.mock.calls[1][0]).toEqual('Testing2')expect(console.log.mock.calls[2][0]).toEqual('Testing3')})})
Shape
module.exports = {printLessons: () => {// your code here...}}
Explanation
request
.https://c0d3.com/api/lessons
.JSON.parse
console.log
for each of the title in the array.Code
const request = require('request')request('https://c0d3.com/api/lessons', (err, res, body) => {const parsedJson = JSON.parse(body)parsedJson.forEach(thisLesson => {console.log(thisLesson.title)})})
Send a request to
[https://www.c0d3.com/api/lessons](https://www.c0d3.com/api/lessons)
and
write all the titles into a file called lessons.html
with the following
content:
<h1>title1</h1><h1>title2</h1>...<h1>titleX</h1>
Asynchronous behavior makes it tricky to split your code into functions, say by
getting the lesson titles in one function and writing them into a file in
another, because the request doesn't return its results. As you get better
with promises, you'll learn how to easily split these tasks up, but for now you
can plan on putting everything that depends on the results inside the request
.
Test
jest.mock('request')const request = require('request')const fs = require('fs')const titlesDoc = require('./lessonTitles')describe('Titles Document', () => {test('should write two titles', () => {request.mockClear()titlesDoc.createTitlesDoc()fs.writeFile = jest.fn()request.mock.calls[0][1]({},{},JSON.stringify([{title: 'c0d3'},{title: 'recursion'}]))expect(fs.writeFile.mock.calls.length).toEqual(1)expect(fs.writeFile.mock.calls[0][1]).toEqual('<h1>c0d3</h1><h1>recursion</h1>')})})
Shape
module.exports = {createTitlesDoc: () => {// your code here...}}
Explanation
request
and fs
.https://c0d3.com/api/lessons
.JSON.parse
.h1
tag.lessons.html
.Code
//lessonTitles.jsconst request = require('request')const fs = require('fs')module.exports = {createTitlesDoc: () => {request('https://c0d3.com/api/lessons', (err, res, body) => {const parsedJson = JSON.parse(body)const titleArray = parsedJson.map(thisLesson => {return thisLesson.title})const titles = titleArray.reduce((acc, thisTitle) => {return `${acc}<h1>${thisTitle}</h1>`}, '')fs.writeFile('lessons.html', titles, () => {})})}}
Did you notice how you had to examine the output from each API to see how to
access the results you needed? It’s easy to do it in node in the terminal; just
request(“<API ADDRESS HERE>”, (err, res, data) => { console.log(data) })
.
Send a request to
https://pokeapi.co/api/v2/pokemon/
and write all the names into a file called names.html
with the following
content:
<h1>name1</h1><h1>name2</h1>...<h1>nameX</h1>
Test
jest.mock('request')const request = require('request')const fs = require('fs')const pokemon = require('./getPokemonNames')describe('Pokemons', () => {test('should write two different pokemon names', () => {request.mockClear()pokemon.getNames()fs.writeFile = jest.fn()request.mock.calls[0][1]({},{},JSON.stringify({results: [{name: 'RahiZzYyY'},{name: 'McGiggles'},{name: 'BrownDynamite'}]}))expect(fs.writeFile.mock.calls.length).toEqual(1)expect(fs.writeFile.mock.calls[0][1]).toEqual('<h1>RahiZzYyY</h1><h1>McGiggles</h1><h1>BrownDynamite</h1>')})})
Shape
module.exports = {getNames: () => {// your code here...}}
Explanation
request
and fs
.https://pokeapi.co/api/v2/pokemon
.JSON.parse
.h1
tag to each pokemon name.names.html
.Code
//getPokemonNames.jsconst request = require('request')const fs = require('fs')request('https://pokeapi.co/api/v2/pokemon/', (err, res, body) => {const parsedJson = JSON.parse(body)const names = parsedJson.results.reduce((acc, pokemon) => {return `${acc}<h1>${pokemon.name}</h1>`}, '')fs.writeFile('names.html', names, () => {})})
You’ll notice that we’re accessing parsedJson.results
; it’s common for APIs
to return one big object that includes both metadata (which page we’re on, how
many results were found, what website this is) and results (as an array).
https://api.openaq.org/v1/countries
to get all countries, and console.log the country with the largest number of
cities.Remember our trick to get reduce
to return 2 pieces of information? Here you
can use it to keep track of both the name of the winning country and the number
of cities it has.
Test
jest.mock('request')const request = require('request')const getCountry = require('./getCountryWithMostCities')describe('Countries', () => {test('find the country with most cities', () => {request.mockClear()getCountry.getMostCities()console.log = jest.fn()request.mock.calls[0][1]({},{},JSON.stringify({results: [{name: 'Narnia',cities: 100},{name: 'SpaceJam',cities: 48},{name: 'Pluto',cities: 250},{name: 'Galaxy',cities: 20}]}))expect(console.log.mock.calls[0][0]).toEqual('Pluto')})})
Shape
module.exports = {getMostCities: () => {// your code here...}}
Explanation
request
https://api.openaq.org/v1/countries
.JSON.parse
.Code
const request = require('request')request('https://api.openaq.org/v1/countries', (err, res, body) => {const countries = JSON.parse(body)const mostCities = countries.results.reduce((acc, elem) => {if (!acc.name || elem.cities > acc.cities)return { name: elem.name, cities: elem.cities }return acc}, {})console.log(mostCities.name)})
https://pokeapi.co/api/v2/pokemon/
and console.log the Pokemon that weighs the most out of the first 20 Pokemon
(if you don't give it a limit, the Pokemon API will default to 20 Pokemon).
Look at the response—you will notice that each Pokemon has a URL. You need to
send another request for each Pokemon to get its weight
.Test
jest.mock('request')const request = require('request')const heaviest = require('./getHeaviestPokemon')describe('Pokemons', () => {test('console.log the heaviest pokemon', () => {request.mockClear()heaviest.heaviestPokemon()console.log = jest.fn()request.mock.calls[0][1]({},{},JSON.stringify({results: [{name: 'Rocky',url: 'testing1'},{name: 'Zoolander',url: 'testing2'},{name: 'Naruto',url: 'testing'}]}))expect(request.mock.calls.length).toEqual(4)request.mock.calls[1][1]({}, {}, JSON.stringify({ weight: 200 }))request.mock.calls[2][1]({}, {}, JSON.stringify({ weight: 300 }))request.mock.calls[3][1]({}, {}, JSON.stringify({ weight: 100 }))expect(console.log.mock.calls[0][0]).toEqual('Heaviest Pokemon is Zoolander at 300 pounds')})})
Shape
module.exports = {getHeaviest: () => {}}
Explanation
request
.https://pokeapi.co/api/v2/pokemon/
JSON.parse
.Code
const request = require('request')request('https://pokeapi.co/api/v2/pokemon/', (err, res, body) => {const parsedJson = JSON.parse(body)const pokemonList = []parsedJson.results.forEach(thisPokemon => {const name = thisPokemon.namerequest(thisPokemon.url, (err, pokeRes, pokeBody) => {const data = JSON.parse(pokeBody)const weight = data.weightpokemonList.push({name: name,weight: weight}) // Shorter syntax: {name, weight}if (parsedJson.results.length === pokemonList.length) {const heaviest = pokemonList.reduce((acc, poke) => {if (poke.weight >= acc.weight) {return poke}return acc}, pokemonList[0])console.log(`Heaviest Pokemon is ${heaviest.name} at ${heaviest.weight} pounds`)}})})})
That was a long function, wasn't it? In the next section, you'll see how to make multiple request calls much cleaner and easier to read. For now, let's look at how we went about coding this:
Examples: The function should only do one thing—examine 20 Pokemon and find the heaviest, then console.log it.
Function shape: We know the main function will be a request
. We'll send 20
additional requests to find the weight of each Pokemon, but because they'll need
the URLs returned by the first request, they'll have to be inside the first
request's callback function.
Only after all 20 weights have been found can we use something like reduce
to
compare them and find the heaviest. That's tricky because we don't know in which
order they'll come back! So each of the 20 callback functions will have to
check if it's the last, and if so, complete the program by comparing the Pokemon
and logging the heaviest.
request('https://pokeapi.co/api/v2/pokemon/', (err, res, body) => {// parse the JSON bodyparsedJson.results.forEach((el) => {request(/*URL for this Pokemon*/, (err, pokeRes, pokeBody) => {// store this Pokemon's data for later// if all 20 weights have been found:// compare the Pokemon// log the heaviest one})})})
Think: We'll need to start an array to keep track of the names and weights
once they've been found outside the forEach
so that each request
can add
to it. Once this array has 20 elements (the same number as the original results
list) we're ready to compare them, which we'll do with reduce
.
Code: Here we create our const pokemonList = []
array, parse the JSON each
time it comes in, push each newly found weight to pokemonList
, and write our
if
condition and reduce
call. We're done!
Test: Running the code as-is should always log Venusaur. If you want to test it on a bigger data set, see the next exercise for how to grab more than 20 Pokemon from the API. For example, if you change the limit to 100, the answer should become golem!
Send a request to
https://pokeapi.co/api/v2/pokemon/
and then send a request to get information for each Pokemon to get its image
(sprites.front_default
). Create an HTML page with 100 Pokemons' names and
images.
(To get 100 Pokemon instead of 20, just pass a parameter into the URL:
https://pokeapi.co/api/v2/pokemon?limit=100
.)
The HTML img
tag is a single tag that takes the URL of an image in its
src
(source) attribute.
<div><p>name1</p><img src="image1" /></div>...<div><p>namex</p><img src="imagex" /></div>
Tests
jest.mock('request')const request = require('request')const fs = require('fs')const pokemons = require('./createPokemonProfile')describe('Pokemons', () => {test('return 3 pokemon profiles', () => {request.mockClear()pokemons.createProfile()fs.writeFile = jest.fn()request.mock.calls[0][1]({},{},JSON.stringify({results: [{name: 'jollyRancher',url: 'testing1'},{name: 'johnnyBravo',url: 'tesing2'},{name: 'blueEyeDragon',url: 'testing3'}]}))expect(request.mock.calls.length).toEqual(4)request.mock.calls[1][1]({},{},JSON.stringify({sprites: {front_default: 'testingPicture1'}}))request.mock.calls[2][1]({},{},JSON.stringify({sprites: {front_default: 'testingPicture2'}}))request.mock.calls[3][1]({},{},JSON.stringify({sprites: {front_default: 'testingPicture3'}}))expect(fs.writeFile.call.length).toEqual(1)expect(fs.writeFile.mock.calls[0][1]).toEqual('<div><p>jollyRancher</p><img src="testingPicture1"/></div><div><p>johnnyBravo</p><img src="testingPicture2"/></div><div><p>blueEyeDragon</p><img src="testingPicture3"/></div>')})})
Shape
module.exports = {createProfile: () => {// your code here...}}
Explanation
request
and fs
.https://pokeapi.co/api/v2/pokemon?limit=100
.JSON.parse
.p
and div
tags to the pokemon's name.namesandimages.html
Code
const request = require('request')const fs = require('fs')request('https://pokeapi.co/api/v2/pokemon?limit=100', (err, res, body) => {const parsedJson = JSON.parse(body)const pokemonList = []parsedJson.results.forEach(thisPokemon => {const pokemonName = thisPokemon.namerequest(thisPokemon.url, (err, pokeRes, pokeBody) => {const data = JSON.parse(pokeBody)pokemonList.push({name: pokemonName,pic: data.sprites.front_default})if (pokemonList.length === parsedJson.results.length) {const htmlStr = pokemonList.reduce((acc, f) => {return `${acc}<div><p>${f.name}</p><img src="${f.pic}"/></div>`}, '')fs.writeFile('namesandimages.html', htmlStr, () => {;``})}})})})
If you are not careful when using the request
library, you may end up in
callback hell.
Code that resembles callback hell is highly discouraged because it can get
really confusing and difficult for other engineers to understand. In fact, our
code in the last couple of exercises was starting to get a little complex with
nested request
calls.
If you submit code that looks like callback hell it will most likely be rejected.
request('https://a.com', (aErr, aRes, aData) => {request('https://b.com', (bErr, bRes, bData) => {request('https://c.com', (cErr, cRes, cData) => {request('https://d.com', (dErr, dRes, dData) => {request('https://e.com', (eErr, eRes, eData) => {// In a large codebase, it may be very difficult// to get to the end of the chain (eData)// since the code is in the middle of the pagecalculateResult(aData, bData, cData, dData, eData)})})})})}) // Bad coding pattern
const onReceiveResponseA = (aErr, aRes, aData) => {// do things with aDatarequest('https://b.com', onReceiveResponseB)}const onReceiveResponseB = (bErr, bRes, bData) => {// do things with bDatarequest('https://c.com', onReceiveResponseC)}const onReceiveResponseC = (cErr, cRes, cData) => {// do things with cDatarequest('https://d.com', onReceiveResponseD)}const onReceiveResponseD = (dErr, dRes, dData) => {// do things with dDatarequest('https://e.com', onReceiveResponseE)}const onReceiveResponseE = (eErr, eRes, eData) => {calculateResult(aData, bData, cData, dData, eData)}request('https://a.com', onReceiveResponseA)
To help junior engineers avoid making mistakes like the above, functions can return an object called a promise. A promise represents the eventual result of an asynchronous action (e.g. making a network request, writing files to the filesystem).
The two most commonly used promise methods are:
then
then
expects a function argument, which will be called when the promise
is done.then
receives the resulting
value and calls the function, passing it this value as an argument.then
returns another promise with the value returned by the
function.catch
catch
expects a function argument, which will be called when the promise
encounters an error.catch
receives an error object and
calls the function, passing it this error as an argumentaxios
is a library that also sends requests, but unlike request
, where you
pass in a function as the second argument, axios returns a promise,
****and you pass the next function into the promise's then
function. This
makes it easy to organize a chain of functions that depend on each other's
results, listing them top-down instead of nesting them and creating callback
hell.
Instead of passing a function as a second argument, you pass the function as an
argument into the return promise's then
function.
// You need to install axios// because it is not a nodejs core (libraries that come installed with nodejs)// To install, run `npm install axios` before running this file.const axios = require('axios')const allData = []const resultOfDataPromise = axios('https://a.com').then(aData => {allData.push(aData)return axios('https://b.com')}).then(bData => {allData.push(bData)return axios('https://c.com')}).then(cData => {allData.push(cData)return axios('https://d.com')}).then(dData => {allData.push(dData)return axios('https://e.com')}).then(eData => {allData.push(eData)// In a large codebase, using returning promise objects can help your// code flow in a logical way (top down) so the last call in the// chain (eData) is towards the end of the page.return calculateResult(allData)}).catch(eErr => {// Find the eErr variable in the callback hell example// and try to understand how this works!})
Another benefit is that a promise's .then
function always returns another
promise, allowing you to chain them. Continuing from the example above, you can
chain from the computed result:
const userNamesPromise = resultOfDataPromise.then(result => {// result is the return value from calling// calculateResult(allData) above.return getUserNames(result)})// To use the final usernames result (i.e. to print it out)userNamesPromise.then(userNames => {console.log(userNames)})
The browser provides you with a function called fetch
that allows you to send
requests and returns a promise. Unlike axios
, you must do an extra step of
parsing the JSON response into an object.
node-fetch
is a not a nodeJS core library (meaning it did not come bundled
when you installed NodeJS), so you need to install the library before using it
by running npm install node-fetch
.
Here's an example of how fetch might be used in a web environment. We are
sending a request to [c0d3.com](http://c0d3.com)
and
displaying how many lessons there are.
Note that when using fetch in the browser we don't need to require
it; the
browser supports fetch automatically. Later examples will include
require('node-fetch')
in case you want to follow along in node.
<h1 class="display"></h1><script>const display = document.querySelector('.display')fetch('https://www.c0d3.com/api/lessons').then( (res) => {return res.json()}).then( (data) => {display.innerText = `there are ${data.length} lessons`})</script>
Notice how we create two promises here: one to get the data using json()
and
another to display it. json()
is different from JSON.parse()
; instead of a
helper function to be used on any text, it's a function of the fetch library,
and runs asynchronously, which is why it needs its own promise.
Complete the below all the way up to e.com so that it has the same result as the axios example!
const fetch = require('node-fetch')const allData = []const resultOfDataPromise = fetch('https://a.com').then( (r) => {// json is a function that turns the response string into a JavaScript data typereturn r.json()}).then( (aData) => {allData.push(aData)return fetch('https://b.com')}).then(
const fetch = require('node-fetch')const allData = []const resultOfDataPromise = fetch('https://a.com').then(r => {return r.json()}).then(aData => {allData.push(aData)return fetch('https://b.com')}).then(bData => {return bData.json()}).then(bData => {allData.push(bData)return fetch('https://c.com')}).then(cData => {return cData.json()}).then(cData => {allData.push(cData)return fetch('https://d.com')}).then(dData => {return dData.json()}).then(dData => {allData.push(dData)return fetch('https://e.com')}).then(eData => {return eData.json()}).then(eData => {allData.push(eData)return calculateResult(allData)})
Because the browser only supports fetch
, software engineers prefer to use
the node-fetch
library instead of axios
when writing code to run on the
computer. It does not matter whether you use axios
or node-fetch
to send
requests from your computer. The choice is yours.
AJAX: Sending a request from the browser
fetch
to send a request from the browser. So you can now
claim that you know ajax too!Many times you may want to send more than one request at once. For example, if you want to look up the prices of 20 houses in your neighborhood (or the weights of 20 Pokemon), you want to send all the requests to get the data at once, then do something with all the data (such as finding the cheapest house or the heaviest Pokemon).
To do this, you can use Promise.all
, which takes in an array of promise
objects and returns a promise. When the then
function runs, the argument
function will get an array of responses.
const fetch = require('node-fetch')const pokeNumbers = [37, // vulpix38, // ninetales39, // jigglypuff40 // wigglytuff]const arrayPromises = pokeNumbers.map(num => {return fetch(`https://pokeapi.co/api/v2/pokemon/${num}`).then(result => {// result is an array of response objects, one for each request// we need to parse the JSON in each resultreturn result.json()})})Promise.all(arrayPromises).then(results => {// results is now an array of objects// we can do something with it, likeresults.forEach(e => {console.log(`${e.name} weighs ${e.weight}`)})})
Here are some questions students asked about Promises that students have asked.
The responses are raw, in chat format.
promise !== data
I'm learning promises, I don't understand why that json() runs asynchronously means that it needs its own promise.
Also, I don't know why fetch is useful in this example. I assume that if it was axios, we don't have to parse it and the code is shorter.
fetch
, No axios
in the front end (Unless
you bring in a new library)I thought that... using .json() instead of JSON.parse() is necessary because .json() makes sure that the computer operates the functions in order
response comes in patches. Sometimes the data a BIG right?
so as data response comes back
it's converting into json
JSON.parse( string )
-> the string has to exist completely first
but with response over the internet, this is a little tricky
because.... responses comes in backets
like... you ask your mom for 100k.... your mom sends you 1k every month
like.... the internet works that way
does that make more sense?
so.... res.json()
-> returns a promise. It takes the responses and as the
packets come in, it puts them together as an object. then resolve when
everything has been received
that's what happens in the background.
Modify your solution to exercise 2 from the previous section to use axios
or node-fetch
: Send a request to
[https://www.c0d3.com/api/lessons](https://www.c0d3.com/api/lessons)
and
write all the titles into a file called lessons.html
with the following
content:
<h1>title1</h1><h1>title2</h1>...<h1>titleX</h1>
Test
jest.mock('node-fetch')jest.mock('fs')const fetch = require('node-fetch')const fs = require('fs')const curriculum = require('./promiseExercise1')describe('c0d3 lessons', () => {it('fs.writeFile should only run once', async () => {fetch.mockClear()fs.writeFile = jest.fn()fetch.mockReturnValue(Promise.resolve({json: () => {return [{ title: 'testing1' }, { title: 'testing2' }]}}))await curriculum.getLessons()expect(fetch.mock.calls.length).toEqual(1)expect(fs.writeFile.mock.calls[0][1]).toEqual('<h1>testing1</h1><h1>testing2</h1>')})})
Shape
module.exports = {getLessons: () => {// your code here...}}
Explanation
fetch
and fs
.h1
tags around lesson titles.fs.writeFile
function.Code
const fetch = require('node-fetch')const fs = require('fs')fetch('https://c0d3.com/api/lessons').then(res => {return res.json()}).then(data => {const titles = data.reduce((acc, f) => {return `${acc}<h1>${f.title}</h1>`}, '')fs.writeFile('lessons.html', titles, () => {})})
Modify your solution to exercise 3 from the previous section to use axios
or node-fetch
: Send a request to
https://pokeapi.co/api/v2/pokemon/
and write all the names into a file called names.html
with the following
content:
<h1>name1</h1><h1>name2</h1>...<h1>nameX</h1>
Test
jest.mock('node-fetch')jest.mock('fs')const fetch = require('node-fetch')const fs = require('fs')const pokemonNames = require('./promiseExercise2')describe('Pokemons', () => {it('should print only two pokemon names', async () => {fetch.mockClear()fs.writeFile = jest.fn()fetch.mockReturnValue(Promise.resolve({json: () => {return {results: [{ name: 'testing1' }, { name: 'testing2' }]}}}))await pokemonNames.getPokemons()expect(fetch.mock.calls.length).toEqual(1)expect(fs.writeFile.mock.calls[0][1]).toEqual('<h1>testing1</h1><h1>testing2</h1>')})})
Shape
module.exports = {getPokemons: () => {// your code here...}}
Explanation
fetch
and fs
.h1
tags to the pokemon names.fs.writeFile
function.Code
const fetch = require('node-fetch')const fs = require('fs')fetch('https://pokeapi.co/api/v2/pokemon/').then(res => {return res.json()}).then(data => {const names = data.results.reduce((acc, f) => {return `${acc}<h1>${f.name}</h1>`}, '')fs.writeFile('names.html', names, () => {})})
axios
or node-fetch
: Send a request to
https://api.openaq.org/v1/countries
to get all countries and console.log the country with the largest number of
cities.Test
jest.mock('node-fetch')const fetch = require('node-fetch')const data = require('./promiseExercise3')describe('Countries', () => {it('should print the country with most cities', async () => {fetch.mockClear()console.log = jest.fn()fetch.mockReturnValue(Promise.resolve({json: () => {return {results: [{name: 'Narnia',cities: 100},{name: 'SpaceJam',cities: 48},{name: 'Pluto',cities: 250},{name: 'Galaxy',cities: 20}]}}}))await data.getCountries()expect(fetch.mock.calls.length).toEqual(1)expect(console.log.mock.calls[0][0]).toEqual('Pluto')})})
Shape
module.exports. = {getCountries: () =>{// your code here..}}
Explanation
fetch
.Console.log
the country with most cities.Code
const fetch = require('node-fetch')const apiUrl = 'https://api.openaq.org/v1/countries'fetch(apiUrl).then(res => {return res.json()}).then(data => {const max = data.results.reduce((acc, f) => {if (f.cities > acc.cities) {return f}return acc}, data.results[0])console.log(max.name)})
axios
or node-fetch
: Send a request to
https://pokeapi.co/api/v2/pokemon/
and the URLs of each of the first 20 Pokemon, and console.log the Pokemon
that weighs the most.Test
jest.mock('node-fetch')const fetch = require('node-fetch')const data = require('./promiseExercise4')describe('pokemons', () => {it('should console.log the heaviest pokemon', async () => {fetch.mockClear()const results = [{results: [{name: 'testing1',url: 'url1'},{name: 'testing2',url: 'url2'},{name: 'testing3',url: 'url3'}]},[{ weight: '250' }, { weight: '120' }, { weight: '360' }]]let resultsIndex = 0console.log = jest.fn()fetch.mockReturnValue(Promise.resolve({json: () => {const result = results[resultsIndex]resultsIndex + 1return result}}))await data.getPokemons()console.log('number of times fetch gets called:',fetch.mock.calls.length)expect(fetch.mock.calls.length).toEqual(4)})})
Shape
module.exports = {getPokemons: () => {// your code here...}}
Explanation
fetch
.console.log
the heaviest pokemon.Code
const fetch = require('node-fetch')fetch('https://pokeapi.co/api/v2/pokemon').then(response => {return response.json()}).then(data => {const fetchPromises = data.results.map(pokemon => {return fetch(pokemon.url).then(pokeRes => {return pokeRes.json()})})return Promise.all(fetchPromises)}).then(dataList => {return dataList.reduce((acc, pokemon) => {if (acc.weight > pokemon.weight) {return acc}return pokemon}, dataList[0])}).then(pokemon => {console.log(pokemon.name)})
[https://www.c0d3.com/api/lessons](https://www.c0d3.com/api/lessons)
and
displays each lesson title
inside an h1
tag.Click the link at the beginning of the problem, and view the page source if you get stuck.
You'll want to accomplish this in steps.
div
's innerHTML
to the string.onclick
event to each element containing the title.official-name
should be inside h1
tags. When you click on
a name, alert the citizen-names
property of that country.Pay close attention to how this API returns its data—the format is a little more complex than the previous APIs we saw.
Almost everything that happens in your browser involves sending requests. To look at all the requests that a website sends out, you can open your console and look at the Network tab. Click on each request to find out more.
Sometimes, developers do not build secure websites. We have prepared a gaming website that does not have proper security.
Exercise: Open up the Network tab in your developer console.
Play this game and win it. After you input your name, observe what request your browser sends to save your score. Change the URL and add your username with a score of 1s to the leaderboard.
Go to Console and add a fetch
request with the correct request query
parameters.
Complete the last two JS3 challenges