Section titled Command handlingCommand handling

Unless your bot project is small, it's not a very good idea to have a single file with a giant if/else if chain for commands. If you want to implement features into your bot and make your development process a lot less painful, you'll want to implement a command handler. Let's get started on that!

For fully functional slash commands, there are three important pieces of code that need to be written. They are:

  1. The individual command files, containing their definitions and functionality.
  2. The command handler, which dynamically reads the files and executes the commands.
  3. The command deployment script, to register your slash commands with Discord so they appear in the interface.

These steps can be done in any order, but all are required before the commands are fully functional.

This page details how to complete Step 2. Make sure to also complete the other pages linked above!

Read first!

Section titled Loading command filesLoading command files

Now that your command files have been created, your bot needs to load these files on startup.

In your index.js file, make these additions to the base template:

JavaScript
TypeScript

_13
import { readdir } from 'node:fs/promises';
_13
import { join } from 'node:path';
_13
import { fileURLToPath } from 'node:url';
_13
import { Client, Collection, Events, GatewayIntentBits } from 'discord.js';
_13
import config from './config.json' assert { type: 'json' };
_13
_13
const client = new Client({ intents: [GatewayIntentBits.Guilds] });
_13
_13
const commands = new Collection();
_13
_13
client.once(Events.ClientReady, () => {
_13
console.log('Ready!');
_13
});

  • The fs module is Node's native file system module. readdir is used to read the commands directory and identify our command files. - The path module is Node's native path utility module. join helps construct paths to access files and directories. One of the advantages of path.join is that it automatically detects the operating system and uses the appropriate joiners. - The url module provides utilities for URL resolution and parsing. fileURLToPath ensuring a cross-platform valid absolute path string.
  • The Collection class extends JavaScript's native Map class, and includes more extensive, useful functionality. Collection is used to store and efficiently retrieve commands for execution.
Tip

Next, using the modules imported above, dynamically retrieve your command files with a few more additions to the index.js file:

JavaScript
TypeScript

_15
const commands = new Collection();
_15
_15
const commandsPath = fileURLToPath(new URL('commands', import.meta.url));
_15
const commandFiles = await readdir(commandsPath).then((files) => files.filter((file) => file.endsWith('.js')));
_15
_15
for (const file of commandFiles) {
_15
const filePath = join(commandsPath, file);
_15
const command = await import(filePath);
_15
// Set a new item in the Collection with the key as the command name and the value as the exported module
_15
if ('data' in command && 'execute' in command) {
_15
commands.set(command.data.name, command);
_15
} else {
_15
console.log(`[WARNING] The command at ${filePath} is missing a required "data" or "execute" property.`);
_15
}
_15
}

First, url.fileURLToPath() helps to construct a path to the commands directory. The fs.readdir() method then reads the path to the directory and returns a Promise which resolves to an array of all the file names it contains, currently ['ping.js', 'server.js', 'user.js']. To ensure only command files get processed, Array.filter() removes any non-JavaScript files from the array.

With the correct files identified, the last step is to loop over the array and dynamically set each command into the commands Collection. For each file being loaded, check that it has at least the data and execute properties. This helps to prevent errors resulting from loading empty, unfinished or otherwise incorrect command files while you're still developing.

Section titled Receiving command interactionsReceiving command interactions

Every slash command is an interaction, so to respond to a command, you need to create a listener for the Client#interactionCreate event that will execute code when your application receives an interaction. Place the code below in the index.js file you created earlier.


_10
client.on(Events.InteractionCreate, (interaction) => {
_10
console.log(interaction);
_10
});

Not every interaction is a slash command (e.g. MessageComponent interactions). Make sure to only handle slash commands in this function by making use of the BaseInteraction#isChatInputCommand() method to exit the handler if another type is encountered. This method also provides type guarding for TypeScript users, narrowing the type from BaseInteraction to ChatInputCommandInteraction.


_10
client.on(Events.InteractionCreate, (interaction) => {
_10
if (!interaction.isChatInputCommand()) return;
_10
console.log(interaction);
_10
});

Section titled Executing commandsExecuting commands

When your bot receives a Client#interactionCreate event, the interaction object contains all the information you need to dynamically retrieve and execute your commands!

Let's take a look at the ping command again. Note the execute() function that will reply to the interaction with "Pong!".


_10
export const data = {
_10
name: 'ping',
_10
description: 'Replies with Pong!',
_10
};
_10
_10
export async function execute(interaction) {
_10
await interaction.reply('Pong!');
_10
}

First, you need to get the matching command from the commands Collection based on the interaction.commandName. If no matching command is found, log an error to the console and ignore the event.

With the right command identified, all that's left to do is call the command's .execute() method and pass in the interaction variable as its argument. Note that the event listener has been made async, allowing Promises to be awaited. In case something goes wrong and the Promise rejects, catch and log any error to the console.


_21
client.on(Events.InteractionCreate, async (interaction) => {
_21
if (!interaction.isChatInputCommand()) return;
_21
_21
const command = commands.get(interaction.commandName);
_21
_21
if (!command) {
_21
console.error(`No command matching ${interaction.commandName} was found.`);
_21
return;
_21
}
_21
_21
try {
_21
await command.execute(interaction);
_21
} catch (error) {
_21
console.error(error);
_21
if (interaction.replied || interaction.deferred) {
_21
await interaction.followUp({ content: 'There was an error while executing this command!', ephemeral: true });
_21
} else {
_21
await interaction.reply({ content: 'There was an error while executing this command!', ephemeral: true });
_21
}
_21
}
_21
});

Section titled Command categoriesCommand categories

So far, all of your command files are in a single commands folder. This is fine at first, but as your project grows, the number of files in the commands folder will too. Keeping track of that many files can be a little tough. To make this a little easier, you can categorize your commands and put them in subfolders inside the commands folder. You will have to make a few changes to your existing code in index.js for this to work out.

If you've been following along, your project structure should look something like this:

Project structure before sorting

After moving your commands into subfolders, it will look something like this:

Project structure after sorting

Make sure you put every command file you have inside one of the new subfolders. Leaving a command file directly under the commands folder will create problems.

Warning

It is not necessary to name your subfolders exactly like we have named them here. You can create any number of subfolders and name them whatever you want. Although, it is a good practice to name them according to the type of commands stored inside them.

Back in your index.js file, where the code to dynamically read command files is, use the same pattern to read the subfolder directories, and then require each command inside them.

JavaScript
Typescript

_19
const commands = new Collection();
_19
_19
const foldersPath = fileURLToPath(new URL('commands', import.meta.url));
_19
const commandFolders = await readdir(foldersPath);
_19
_19
for (const folder of commandFolders) {
_19
const commandsPath = join(foldersPath, folder);
_19
const commandFiles = await readdir(commandsPath).then((files) => files.filter((file) => file.endsWith('.js')));
_19
for (const file of commandFiles) {
_19
const filePath = join(commandsPath, file);
_19
const command = await import(filePath);
_19
// Set a new item in the Collection with the key as the command name and the value as the exported module
_19
if ('data' in command && 'execute' in command) {
_19
commands.set(command.data.name, command);
_19
} else {
_19
console.log(`[WARNING] The command at ${filePath} is missing a required "data" or "execute" property.`);
_19
}
_19
}
_19
}

That's it! When creating new files for commands, make sure you create them inside one of the subfolders (or a new one) in the commands folder.

Section titled Next stepsNext steps

Your command files are now loaded into your bot, and the event listener is prepared and ready to respond. In the next section, we cover the final step - a command deployment script you'll need to register your commands so they appear in the Discord client.

Section titled Resulting codeResulting code

It also includes some bonus commands!