Command handling
Unless your bot project is a small one, 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!
Here are the base files and code we'll be using:
npm install @discordjs/rest discord-api-types
yarn add @discordjs/rest discord-api-types
pnpm add @discordjs/rest discord-api-types
const { Client, Intents } = require('discord.js');
const { token } = require('./config.json');
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;
if (commandName === 'ping') {
await interaction.reply('Pong!');
} else if (commandName === 'beep') {
await interaction.reply('Boop!');
}
});
client.login(token);
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
const { REST } = require('@discordjs/rest');
const { Routes } = require('discord-api-types/v9');
const { clientId, guildId, token } = require('./config.json');
const commands = [];
const rest = new REST({ version: '9' }).setToken(token);
rest.put(Routes.applicationGuildCommands(clientId, guildId), { body: commands })
.then(() => console.log('Successfully registered application commands.'))
.catch(console.error);
2
3
4
5
6
7
8
9
10
11
{
"clientId": "123456789012345678",
"guildId": "876543210987654321",
"token": "your-token-goes-here"
}
2
3
4
5
Individual command files
Your project directory should look something like this:
discord-bot/
├── node_modules
├── config.json
├── deploy-commands.js
├── index.js
├── package-lock.json
└── package.json
Create a new folder named commands
, which is where you'll store all of your commands.
We'll be using utility methods from the @discordjs/builders
open in new window package to build the slash command data, so open your terminal and install it.
npm install @discordjs/builders
yarn add @discordjs/builders
pnpm add @discordjs/builders
Next, create a commands/ping.js
file for your ping command:
const { SlashCommandBuilder } = require('@discordjs/builders');
module.exports = {
data: new SlashCommandBuilder()
.setName('ping')
.setDescription('Replies with Pong!'),
async execute(interaction) {
await interaction.reply('Pong!');
},
};
2
3
4
5
6
7
8
9
10
You can go ahead and do the same for the rest of your commands, putting their respective blocks of code inside the execute()
function.
TIP
module.exports
open in new window is how you export data in Node.js so that you can require()
open in new window it in other files.
If you need to access your client instance from inside a command file, you can access it via interaction.client
. If you need to access external files, packages, etc., you should require()
them at the top of the file.
Reading command files
In your index.js
file, make these additions:
const fs = require('node:fs');
const path = require('node:path');
const { Client, Collection, Intents } = require('discord.js');
const { token } = require('./config.json');
const client = new Client({ intents: [Intents.FLAGS.GUILDS] });
client.commands = new Collection();
2
3
4
5
6
7
8
We recommend attaching a .commands
property to your client instance so that you can access your commands in other files. The rest of the examples in this guide will follow this convention.
TIP
fs
open in new window is Node's native file system module. Collection
open in new window is a class that extends JavaScript's native Map
open in new window class, and includes more extensive, useful functionality.
TIP
path
open in new window is Node's native path utility module. It helps construct paths to access files and directories. Instead of manually writing './currentDirectory/fileYouWant'
everywhere, one can instead use path.join()
and pass each path segment as an argument. Note however, you should omit '/'
or other path segment joiners as these may be different depending on the operating system running your code. One of the advantages of the path
module is that it automatically detects the operating system and uses the appropriate joiners.
Next you will learn how to dynamically retrieve your command files. First you'll need to get the path to the directory that stores your command files. The node core module 'path'open in new window and it's join()
method will help to construct a path and store it in a constant so you can reference it later. Following that, the fs.readdirSync()
open in new window method will return an array of all the file names in the directory, e.g. ['ping.js', 'beep.js']
. To ensure only command files get returned, use Array.filter()
to leave out any non-JavaScript files from the array. With that array, loop over it and dynamically set your commands to the client.commands
Collection.
client.commands = new Collection();
const commandsPath = path.join(__dirname, 'commands');
const commandFiles = fs.readdirSync(commandsPath).filter(file => file.endsWith('.js'));
for (const file of commandFiles) {
const filePath = path.join(commandsPath, file);
const command = require(filePath);
// Set a new item in the Collection
// With the key as the command name and the value as the exported module
client.commands.set(command.data.name, command);
}
2
3
4
5
6
7
8
9
10
11
12
Use the same approach for your deploy-commands.js
file, but instead .push()
to the commands
array with the JSON data for each command.
const fs = require('node:fs');
const path = require('node:path');
const { REST } = require('@discordjs/rest');
const { Routes } = require('discord-api-types/v9');
const { clientId, guildId, token } = require('./config.json');
const commands = [];
const commandsPath = path.join(__dirname, 'commands');
const commandFiles = fs.readdirSync(commandsPath).filter(file => file.endsWith('.js'));
for (const file of commandFiles) {
const filePath = path.join(commandsPath, file);
const command = require(filePath);
commands.push(command.data.toJSON());
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
Dynamically executing commands
You can use the client.commands
Collection setup to retrieve and execute your commands! Inside the interactionCreate
event, delete the if
/else if
chain of commands and replace it with this:
client.on('interactionCreate', async interaction => {
if (!interaction.isCommand()) return;
const command = client.commands.get(interaction.commandName);
if (!command) return;
try {
await command.execute(interaction);
} catch (error) {
console.error(error);
await interaction.reply({ content: 'There was an error while executing this command!', ephemeral: true });
}
});
2
3
4
5
6
7
8
9
10
11
12
13
14
First, fetch the command in the Collection with that name and assign it to the variable command
. If the command doesn't exist, it will return undefined
, so exit early with return
. If it does exist, call the command's .execute()
method, and pass in the interaction
variable as its argument. In case something goes wrong, log the error and report back to the member to let them know.
And that's it! Whenever you want to add a new command, make a new file in your commands
directory, name it the same as the slash command, and then do what you did for the other commands. Remember to run node deploy-commands.js
to register your commands!
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.