Fetch Data from an API with JavaScript
Dad jokes are way funnier when you can build an app with them
This TinyApp is DadJoke Central. It displays dad jokes on a web page. The TinyApp is powered by a free API called ICanHazDadJoke. While this is all fun, this TinyApp shows how a JavaScript frontend can communicate with a backend -- pretty powerful stuff!
Meet DadJoke Central
This TinyApp comprises of just an index.html
, styles.css
, and index.js
. The HTML and CSS is very simple, and most of the fun happens in the JavaScript. Here's the sandbox:
Structure and Style
HTML provides structure to our application, and CSS gives it a nice coat of paint. Starting with the HTML, we see a title, subtitle, a button, and a placeholder for where the jokes are generated.
<h1>๐ DadJoke Central ๐</h1>
<p>The TinyApp for Dad Jokes.</p>
<button id="jokeButton">Get a Joke!</button>
<p id="joke">~ a joke will be here ~</p>
The important elements to take note of in this HTML are:
- the button with an
id
ofjokeButton
. We'll use this button to trigger the function which grabs our dad joke. - the paragraph tag with an
id
ofjoke
. We'll look to update the content of this paragraph with a joke that we retrieve from the API.
Our styling is fairly simple too. It's mostly adding some color and fonts, so I won't break it down, but I'll include it here:
body {
font-family: "Arbutus Slab", serif;
color: #4a4a4a;
background: #eee;
}
#joke {
color: darkblue;
font-size: 1.2em;
font-weight: bold;
}
Let's break down what happens next, and talk about APIs!
What does an API do?
Why are APIs important, and why do we hear about them everywhere? APIs allow developers to have their own application talk to other applications that they haven't written. It makes building web apps modular. When we talk about web APIs, usually that means that some data is being sent to a frontend or another backend.
In this TinyApp, we're using ICanHazDadJoke, which is an application that serves data in the JSON format. A JSON API is almost the de-facto way that modern web applications talk to one another. This is because most languages can understand and parse JSON. You can almost imagine it as a translation bridge between a frontend and a backend.
In a sense, this TinyApp is a frontend that uses ICanHazDadJoke as a backend; it provides the data we're looking to display.
Fetch and Asynchronous JavaScript
To grab a joke, we need to make a request to the ICanHazDadJoke API. We can do this using Fetch in JavaScript. The following code looks dense and scary, but take a look and see if you can figure out what's going on! I'll break it down below.
const fetchJoke = async () => {
try {
const response = await fetch("https://icanhazdadjoke.com/", {
headers: {
Accept: "application/json"
}
});
if (!response.ok) {
const message = `Error: ${response.status}, ${response.statusText}`;
const error = new Error(message);
throw error;
}
const joke = await response.json();
return joke;
} catch (error) {
console.error(`ERROR \n${error.message}`);
}
};
This is the fetchJoke()
function, which (as we can tell by the function name), returns a Joke object. You'll notice a keyword async
written before the arrow function begins. This indicates that the function will return a JavaScript Promise. Promises are the subject of a whole other TinyApp, so I won't get into the dense particulars of it.
Rather, understand that a Promise communicates that some return value is on its way from this function, it's promised to arrive. It just needs a bit of time to resolve. To resolve a promise, we have to await
its response. This tells JavaScript to wait on the resolution of a promise before proceeding.
We then open up a try...catch
block. This is how JavaScript practices error handling. If there are any errors that happen within our try
block, they will be caught and the code within the catch
block will be run. Since communicating with an API is happening externally, and there can be a variety of things that can mess up, using a try
and catch
helps our errors to be clearer.
When we enter the try
block, we see a response
variable that contains the value of the fetch
request.
const response = await fetch("https://icanhazdadjoke.com/", {
headers: {
Accept: "application/json"
}
});
Fetch is an asynchronous function, so we need to await
its response in order for it to resolve. It takes a few different arguments (check out the further reading section to see more details). The most important one is the first, the url that data is being fetched from.
If we read the docs for the ICanHazDadJoke API, it specifies that we need to fetch from https://icanhazdadjoke.com/ and that we need to accept a header in 'application/json' if we're trying to parse the data as JSON. When I was writing this TinyApp, I missed that in the documentation, resulting in many vague errors! After studying the errors and revisiting the page, I realized I had forgotten to include headers
!
The headers
object in the fetch
therefore sets the Accept
value to application/json
. When we run this code, we'll get a response object in JSON format from the Dad Joke API.
The next if block in our code could look a little over the top, but it's important for catching errors that occur in our try
block.
if (!response.ok) {
const message = `Error: ${response.status}, ${response.statusText}`;
const error = new Error(message);
throw error;
}
If the response isn't ok !response.ok
- (which means that the response status from the API isn't a 200) - we throw an error with an error message, containing the status of the error and the statusText
. Adding this piece gives more description about what went wrong and will speed up our time to fix it.
If that all goes well and there are no errors, we get to translate our response
in JSON to JavaScript. That way, we can actually share the joke on the screen.
const joke = await response.json();
return joke;
We set a variable joke
equal to our response.json()
which magically parses JSON into JavaScript. Since .json()
is also asynchronous, we will have to await
it to resolve as well. Once it's resolved, we return the joke
variable (a joke object).
Displaying our Joke
Whew! We finally have our joke object ready to display on the screen.
First, let's get some references to the HTML where we want to display the joke, and the button that will trigger us to request a new joke from the API.
let jokePara = document.querySelector("#joke");
let jokeButton = document.querySelector("#jokeButton");
If you remember from earlier in the article, we had a paragraph tag with an id of "joke" and button tag with an id of "jokeButton". These two variables refer to those HTML elements, which we can now manipulate.
To do that, we need to create a function which changes the paragraph tag's contents to the joke that we receive from the API. There are a couple new keywords here, so let's dig in.
const renderJoke = () => {
fetchJoke().then((jokeObject) => {
jokePara.innerHTML = jokeObject.joke;
});
};
While await
is certainly one way to let Promises resolve, .then()
does the same. While await
and .then()
aren't exactly the same, they similarly wait for a Promise to resolve before proceeding. In this function, renderJoke()
, we first call fetchJoke()
. It returns a joke object that's readable in JavaScript, but the function is itself asynchronous. This makes the .then()
pattern very useful here.
First, we wait for fetchJoke()
to return a JokeObject. Then, with reference to that object, we set the innerHTML
of our paragraph tag jokePara
to the joke
value.
It's time for the final piece. We need to trigger this renderJoke()
function when the button on the UI is clicked. That means we need to listen for a click. By adding an eventListener
to our jokeButton
element, we can tell renderJoke()
to run on every click.
jokeButton.addEventListener("click", renderJoke);
We did it! Laugh your merry way as you connect APIs to your JavaScript projects in the future!