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);
1
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);
1
2
3
4
5
6
7
8
9
10
11
{
	"clientId": "123456789012345678",
	"guildId": "876543210987654321",
	"token": "your-token-goes-here"
}
1
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/buildersopen 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!');
	},
};
1
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.exportsopen 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('fs');
const { Client, Collection, Intents } = require('discord.js');
const { token } = require('./config.json');

const client = new Client({ intents: [Intents.FLAGS.GUILDS] });

client.commands = new Collection();
 
 




 
1
2
3
4
5
6
7

TIP

fsopen in new window is Node's native file system module. Collectionopen in new window is a class that extends JavaScript's native Mapopen in new window class, and includes more extensive, useful functionality.

This next step is how to dynamically retrieve your command files. The fs.readdirSync()open in new window method will return an array of all the file names in a 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 commandFiles = fs.readdirSync('./commands').filter(file => file.endsWith('.js'));

for (const file of commandFiles) {
	const command = require(`./commands/${file}`);
	// 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);
}

 

 
 
 
 
 
 
1
2
3
4
5
6
7
8
9

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('fs');
const { REST } = require('@discordjs/rest');
const { Routes } = require('discord-api-types/v9');
const { clientId, guildId, token } = require('./config.json');

const commands = [];
const commandFiles = fs.readdirSync('./commands').filter(file => file.endsWith('.js'));

for (const file of commandFiles) {
	const command = require(`./commands/${file}`);
	commands.push(command.data.toJSON());
}
 





 

 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12

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 });
	}
});



 
 
 
 
 
 
 
 
 
 

1
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.