Using a REST API
REST APIs are extremely popular on the web and allow you to freely grab a site's data if it has an available API over an HTTP connection.
Making HTTP requests with Node
In these examples, we will be using undiciopen in new window, an excellent library for making HTTP requests.
To install undici, run the following command:
npm install undici
yarn add undici
pnpm add undici
Skeleton code
To start off, you will be using the following skeleton code. Since both the commands you will be adding in this section require an interaction with external APIs, you will defer the reply, so your application responds with a "thinking..." state. You can then edit the reply once you got the data you need:
const { Client, Intents, MessageEmbed } = require('discord.js');
const client = new Client({ intents: [Intents.FLAGS.GUILDS] });
client.once('ready', () => {
console.log('Ready!');
});
client.on('interactionCreate', async interaction => {
if (!interaction.isCommand()) return;
const { commandName } = interaction;
await interaction.deferReply();
// ...
});
client.login('your-token-goes-here');
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
TIP
We're taking advantage of destructuring in this tutorial to maintain readability.
Using undici
Undici is a Promise-based HTTP/1.1 client, written from scratch for Node.js. If you aren't already familiar with Promises, you should read up on them here.
In this tutorial, you will be making a bot with two API-based commands using the random.catopen in new window and Urban Dictionaryopen in new window APIs.
On top of your file, import the library function you will be using:
const { request } = require('undici');
Retrieving the JSON response from a request
To get the data from within the response object, you can define the following helper function (it concatenates all the body chunks and parses it as an object) above your client events. Note that the function returns a Promise you need to handle.
async function getJSONResponse(body) {
let fullBody = '';
for await (const data of body) {
fullBody += data.toString();
}
return JSON.parse(fullBody);
}
2
3
4
5
6
7
8
9
Random Cat
Random cat's API is available at https://aws.random.cat/meowopen in new window and returns a JSONopen in new window response. To actually fetch data from the API, you're going to do the following:
const catResult = await request('https://aws.random.cat/meow');
const { file } = await getJSONResponse(catResult.body);
2
If you just add this code, it will seem like nothing happens. What you do not see, is that you are launching a request to the random.cat server, which responds some JSON data. The helper function parses the response data to a JavaScript object you can work with. The object will have a file
property with the value of a link to a random cat image.
Next, you will implement this approach into an application command:
client.on('interactionCreate', async interaction => {
// ...
if (commandName === 'cat') {
const catResult = await request('https://aws.random.cat/meow');
const { file } = await getJSONResponse(catResult.body);
interaction.editReply({ files: [file] });
}
});
2
3
4
5
6
7
8
So, here's what's happening in this code:
- Your application sends a
GET
request to random.cat. - random.cat sees the request and gets a random file url from their database.
- random.cat then sends that file's URL as a JSON object that contains a link to the image.
- undici receives the response and you parse it with
getJSONResponse()
. - Your application then attaches the image and sends it in Discord.
Urban Dictionary
Urban Dictionary's API is available at https://api.urbandictionary.com/v0/defineopen in new window, accepts a term
parameter, and returns a JSON response.
The following code will fetch data from this api:
// ...
client.on('interactionCreate', async interaction => {
// ...
if (commandName === 'urban') {
const term = interaction.options.getString('term');
const query = new URLSearchParams({ term });
const dictResult = await request(`https://api.urbandictionary.com/v0/define?${query}`);
const { list } = await getJSONResponse(dictResult.body);
}
});
2
3
4
5
6
7
8
9
10
11
Here, you are using JavaScript's native URLSearchParams classopen in new window to create a query stringopen in new window for the URL so that the Urban Dictionary server can parse it and know what you want to look up.
If you were to do /urban hello world
, then the URL would become https://api.urbandictionary.com/v0/define?term=hello%20world since the string "hello world"
is encoded.
You can get the respective properties from the returned JSON. If you were to view it in your browser, it usually looks like a bunch of mumbo jumbo. If it doesn't, great! If it does, then you should get a JSON formatter/viewer. If you're using Chrome, JSON Formatteropen in new window is one of the more popular extensions. If you're not using Chrome, search for "JSON formatter/viewer <your browser>" and get one.
Now, if you look at the JSON, you can see that it has a list
property, which is an array of objects containing various definitions for the term (maximum 10). Something you always want to do when making API-based commands is to handle the case when no results are available. So, if you throw a random term in there (e.g. njaksdcas
) and then look at the response the list
array should be empty. Now you are ready to start writing!
As explained above, you'll want to check if the API returned any answers for your query, and send back the definition if that's the case:
if (commandName === 'urban') {
// ...
if (!list.length) {
return interaction.editReply(`No results found for **${term}**.`);
}
interaction.editReply(`**${term}**: ${list[0].definition}`);
}
2
3
4
5
6
7
8
Here, you are only getting the first object from the array of objects called list
and grabbing its definition
property.
If you've followed the tutorial, you should have something like this:
Now, you can make it an embed for easier formatting.
You can define the following helper function at the top of your file. In the code below, you can use this function to truncate the returned data and make sure the embed doesn't error, because field values exceed 1024 characters.
const trim = (str, max) => (str.length > max ? `${str.slice(0, max - 3)}...` : str);
And here is how you can build the embed from the API data:
const [answer] = list;
const embed = new MessageEmbed()
.setColor('#EFFF00')
.setTitle(answer.word)
.setURL(answer.permalink)
.addFields({ name: 'Definition', value: trim(answer.definition, 1024) }, { name: 'Example', value: trim(answer.example, 1024) }, { name: 'Rating', value: `${answer.thumbs_up} thumbs up. ${answer.thumbs_down} thumbs down.` });
interaction.editReply({ embeds: [embed] });
2
3
4
5
6
7
8
9
Now, if you execute that same command again, you should get this:
Resulting code
If you want to compare your code to the code we've constructed so far, you can review it over on the GitHub repository here open in new window.