scattering clouds

Coding practice 7: Interacting with APIs using vanilla JS.

As a continuation from my last blog post, I want to learn how to use the vanilla JavaScript (JS) Fetch API to interact with an external API or service.

Why JS? Because it can be used directly inside the browser console, rather than having to install a bunch of crap on my laptop like Python, PHP, Java, and whatnot.

I could also use curl via Command Prompt (cmd), which comes built-in to Windows 10 and newer, but that would require me to learn both curl and cmd, which I am not interested in at the moment. So vanilla JS it is.

The first external API.

For the purpose of this exercise, I will be calling the API from icanhazdadjoke.com, which is created and maintained by C653 Labs. I chose this API because:

  • GET access to the API is free with no authentication required,
  • they claim to handle “over 250,000 unique API requests per day”,
  • they claim to have the largest curated selection of dad jokes on the internet,
  • and I love the wordplay used in dad jokes,

which makes icanhazdadjoke.com one of the best websites out there imo. 😉

The only requirement (or more of a general courtesy) to using their API is to set a custom User-Agent header for all requests, which helps them monitor the usage of their API and potentially identify bad actors who abuse their service.

In my case, I will be using the following header

User-Agent: Scattering Clouds blog (https://scatteringclouds.com/coding-practice-7/)

to identify my usage of their API.

The JavaScript Fetch API.

The JS Fetch API makes use of JS Promises, which we explored in my last blog post.

The API itself consists of a single function called fetch(), which can be used to fetch resources from across the internet. It returns a Promise that will (hopefully) resolve to the Response of the request.

const URL = "https://link-to-some-resource.com";

let response = fetch(URL);

By default, the fetch() method will send out a GET request. We can customise the request method using an additional options object like thus:

let response = fetch(URL, options);

let response = fetch(URL, {
	method: "POST"
});

Supported HTTP request methods include DELETE, GET, HEAD, OPTIONS, POST, PUT, and PATCH. It is important to note that these request methods are actually case-sensitive as per the HTTP specifications.

While most browsers are quite lenient and will allow you to get away with using “delete”, “get”, “head”, “options”, “post”, and “put” in fetch(), you will most likely run into unexpected problems if you tried to use “patch” instead of “PATCH”.

We can also include additional options such as header fields and an optional request body, for example:

let response = fetch(URL, {
	method: "POST",
	headers: {
		"Content-Type": "text/plain; charset=UTF-8",
		"User-Agent": "Scattering Clouds blog (https://scatteringclouds.com/coding-practice-7/)"
	},
	body: "The (optional) request body."
});

Handling and reading the Response.

Since fetch() returns a Promise, we can use then() to handle the resulting Response object that is returned when the Promise resolves.

let response = fetch(URL);

response.then((response) => {
	return response.text();
}).then((text) => {
	console.log(text);
});

Alternatively, you can handle the Response using the async/await syntax, which makes the code a whole lot easier to read and write.

async function makeRequest(URL) {
	let response = await fetch(URL);
	let text = await response.text();
	console.log(text);
}

It is important to note here that the fetch() Promise will only reject when there is a network error. It does not reject on HTTP errors, such as 404 (not found error).

We need to check the Response object to determine the outcome of the HTTP request, which can be done by looking at Response.ok and/or Response.status.

  • Response.ok is a boolean that indicates whether the response was successful (i.e. status code in the range 200 – 299) or not.
  • Response.status will return the status code of the respose, i.e. 200 – 299 for success, 400 – 499 for client errors, and 500 – 599 for server errors.

Once we have determined that we have a valid and successful response, we can get the contents of the response using methods like .text() or .json(). These methods will return another Promise that resolves the contents of the Response to the corresponding representation format (e.g. a string of text or a JSON object).

Some working examples with HTTP GET.

Note: The API documentation for icanhazdadjoke.com can be found here.

Plain text request.

We can try and get a random joke using the icanhazdadjoke.com API with the following code:

const showJoke = async () => {
	let response = await fetch("https://icanhazdadjoke.com/", {
		method: "GET",
		headers: {
			"Accept": "text/plain",
			"User-Agent": "Scattering Clouds blog (https://scatteringclouds.com/coding-practice-7/)"
		}
	});
	if(!response.ok){
		alert(`Fetch failed with status code ${response.status}.`);
		return;
	}
	let joke = await response.text();
	alert(`Fetch succeeded! The joke is: \n\n${joke}`);
}

showJoke();

JSON request.

The icanhazdadjoke.com API also supports JSON responses, which can be obtained using the following code:

const showJokeJSON = async () => {
	let response = await fetch("https://icanhazdadjoke.com/", {
		method: "GET",
		headers: {
			"Accept": "application/json",
			"User-Agent": "Scattering Clouds blog (https://scatteringclouds.com/coding-practice-7/)"
		}
	});
	if(!response.ok){
		alert(`Fetch failed with status code ${response.status}.`);
		return;
	}
	let json = await response.json();
	alert(`Fetch succeeded! The JSON is: \n\n${JSON.stringify(json, null, 4)}`);
}

showJokeJSON();

Image request.

The icanhazdadjoke.com API also allows us to fetch dad jokes as images, although it requires us to know the ID of the joke beforehand.

We can use the following code to first get a random joke through their JSON API, and then use the joke ID provided within the JSON to perform another fetch() request for the image.

Once we have the image data, it’s just a matter of displaying it, which can be done by adding it to the src of an image element that is already present on the page.

const getJokeJSON = async () => {
	let response = await fetch("https://icanhazdadjoke.com/", {
		method: "GET",
		headers: {
			"Accept": "application/json",
			"User-Agent": "Scattering Clouds blog (https://scatteringclouds.com/coding-practice-7/)"
		}
	});
	if(!response.ok){
		alert(`Fetch failed with status code ${response.status}.`);
		return;
	}
	let json = await response.json();
	return json;
}

const showJokeImage = async () => {
	let jokeJSON = await getJokeJSON();
	let jokeID = jokeJSON.id;
	let response = await fetch(`https://icanhazdadjoke.com/j/${jokeID}.png`, {
		headers: {
			"User-Agent": "Scattering Clouds blog (https://scatteringclouds.com/coding-practice-7/)"
		}
	});
	if(!response.ok){
		alert(`Fetch failed with status code ${response.status}.`);
		return;
	}
	let imageBlob = await response.blob();
	let objectURL = URL.createObjectURL(imageBlob);
	
	let image = document.querySelector("#jokeImage");
	if(image.src !== "") URL.revokeObjectURL(image.src);
	document.querySelector("#jokeImageWrapper").style.display = "";
	image.src = objectURL;
	image.alt = jokeJSON.joke;
}

showJokeImage();

The second external API.

So far we have only interacted with APIs using GET requests, which is about as far as we can get with free and public third-party APIs. Nobody will accept POST, PATCH, or DELETE requests to their API without some form of authentication in place, because allowing so would leave them open to all manners of abuse.

Not to mention the fact that building, hosting, and maintaining a public API costs money, and it is actually one of the most expensive parts of web development. Even if you can keep your costs to as low as $0.0001 per API call, it can still add up quickly if people make several hundred thousand requests to your API each day.

This is why you don’t often see people offering full API access for free – it usually comes with a recurring paid subscription and strict limits. Overages will cost extra!

For the second part of this article, I will use an account with Flotiq to experiment with adding, editing, and deleting entries using a REST API. Flotiq has a free tier that also comes with a 14-day trial of their basic plan, which is quite sufficient for our needs.

Setting up a custom Content Type.

I won’t go into too much detail here about how to set up a custom Content Type definition in Flotiq. Their graphical user interface (GUI) is straightforward and intuitive enough and it should be easy to figure things out yourself.

For the purpose of this exercise, I simply created a Content Type with the Label “testing” with two Content Type Properties labelled “title” and “description”, which are both of the type “Text”. The API Name and property keys will be automatically generated based on their corresponding labels.

Once the Content Type is created and saved, Flotiq will automatically generate the corresponding documentation via the “API doc” option in the editor menu.

You will also need to open your account options menu and click on “API Keys” to find your secret “Read and write API KEY”, which is just a mix of letters and numbers.

Once you have all of the above ready and set up, we can begin experimenting with Flotiq’s API.

Some example code for POST, PATCH, and DELETE.

Adding a new entry.

We can use a POST request to add a new entry (a.k.a. a Content Object) to the custom Content Type we just created. Here’s the example code I used:

const apiKey = "enter_your_read_and_write_API_KEY_here_between_the_quotation_marks";

const addEntry = async () => {
	let response = await fetch("https://api.flotiq.com/api/v1/content/testing", {
		method: "POST",
		headers: {
			"Content-type": "application/json; charset=UTF-8",
			"X-AUTH-TOKEN": apiKey
		},
		body: JSON.stringify({
			"title": "A random title",
			"description": "Hello world!"
		})
	});
	
	if(!response.ok) console.log(`PUT failed with status code ${response.status}. \n\n${JSON.stringify(await response.json(), null, 4)}.`);
	else console.log(`New entry added! The ID of this entry is ${(await response.json()).id}.`);
}

addEntry();

Try copy and pasting the above code into your browser console. If everything is set up correctly, the PUT request should execute successfully and add a new entry to the custom Content Type in Flotiq.

The ID of each newly added entry is unique and can be automatically generated by Flotiq. You can add more entries and make each new entry more distinctive by editing the “title” and “description” within the body of the fetch() request.

If the code output is instead “PUT failed with status code 401.”, go back and check to see if you have copy and pasted your own “Read and write API KEY” to the apiKey variable in the first line of the above code.

Updating an existing entry.

According to Flotiq’s automatically generated API doc, it is possible to update an existing entry using a PATCH request if you know its ID, which can be obtained using a GET request to list all existing objects, or simply by reading from the Flotiq editor.

Here’s the example code I wrote:

const apiKey = "enter_your_read_and_write_API_KEY_here_between_the_quotation_marks";
const entryID = "enter_the_id_of_an_existing_entry_here";

const updateEntry = async () => {
	let response = await fetch(`https://api.flotiq.com/api/v1/content/testing/${entryID}`, {
		method: "PATCH",
		headers: {
			"Content-type": "application/json; charset=UTF-8",
			"X-AUTH-TOKEN": apiKey
		},
		body: JSON.stringify({
			"description": "an updated description"
		})
	});
	
	if(!response.ok) console.log(`PATCH failed with status code ${response.status}. \n\n${JSON.stringify(await response.json(), null, 4)}.`);
	else console.log("Entry updated!");
}

updateEntry();

Deleting an entry.

Likewise, you can delete an existing entry using a DELETE request if you know its ID. Here’s the code:

const apiKey = "enter_your_read_and_write_API_KEY_here_between_the_quotation_marks";
const entryID = "enter_the_id_of_an_existing_entry_here";

const deleteEntry = async () => {
	let response = await fetch(`https://api.flotiq.com/api/v1/content/testing/${entryID}`, {
		method: "DELETE",
		headers: {
			"X-AUTH-TOKEN": apiKey
		}
	});
	
	if(!response.ok) console.log(`DELETE failed with status code ${response.status}. \n\n${JSON.stringify(await response.json(), null, 4)}.`);
	else console.log("Entry deleted!");
}

deleteEntry();

back to top