How to split discord.js commands into separate files
Build a simple discord.js v14 command handler that loads each slash command from its own file in a commands folder, registers them and runs them.
Updated · 1 min read
Once a discord.js bot has more than a few commands, put each one in its own file inside a commands folder and load them all at startup. This keeps index.js short and makes adding a command as simple as adding a file.
Project layout
my-bot/
index.js
package.json
.env
commands/
ping.js
echo.js
A command file
Each file exports a data builder and an execute function:
// commands/ping.js
const { SlashCommandBuilder } = require('discord.js');
module.exports = {
data: new SlashCommandBuilder()
.setName('ping')
.setDescription('Replies with Pong!'),
async execute(interaction) {
await interaction.reply(`Pong! ${interaction.client.ws.ping} ms`);
},
};
// commands/echo.js
const { SlashCommandBuilder } = require('discord.js');
module.exports = {
data: new SlashCommandBuilder()
.setName('echo')
.setDescription('Repeats your text')
.addStringOption((o) => o.setName('text').setDescription('Text to repeat').setRequired(true)),
async execute(interaction) {
await interaction.reply(interaction.options.getString('text', true));
},
};
The loader in index.js
require('dotenv').config();
const fs = require('node:fs');
const path = require('node:path');
const { Client, Collection, Events, GatewayIntentBits, MessageFlags } = require('discord.js');
const client = new Client({ intents: [GatewayIntentBits.Guilds] });
client.commands = new Collection();
const commandsDir = path.join(__dirname, 'commands');
for (const file of fs.readdirSync(commandsDir).filter((f) => f.endsWith('.js'))) {
const command = require(path.join(commandsDir, file));
if ('data' in command && 'execute' in command) {
client.commands.set(command.data.name, command);
} else {
console.warn(`Skipping ${file}: missing data or execute`);
}
}
client.once(Events.ClientReady, async (readyClient) => {
const body = client.commands.map((c) => c.data.toJSON());
await readyClient.application.commands.set(body, process.env.GUILD_ID || undefined);
console.log(`Logged in as ${readyClient.user.tag}, registered ${body.length} commands`);
});
client.on(Events.InteractionCreate, async (interaction) => {
if (!interaction.isChatInputCommand()) return;
const command = client.commands.get(interaction.commandName);
if (!command) return;
try {
await command.execute(interaction);
} catch (error) {
console.error(`Error in /${interaction.commandName}:`, error);
const msg = { content: 'Something went wrong running that command.', flags: MessageFlags.Ephemeral };
if (interaction.replied || interaction.deferred) await interaction.followUp(msg).catch(() => {});
else await interaction.reply(msg).catch(() => {});
}
});
client.login(process.env.DISCORD_TOKEN);
Key points:
path.join(__dirname, 'commands')finds the folder relative toindex.js, so it works however the server starts the process.- Commands are registered from the same files you execute, so the two never drift apart.
- With
GUILD_IDset in.env, commands register to that server only (instant updates for testing). Remove it to register globally. See discord.js slash commands. - The try/catch stops one broken command from crashing the bot. See bot keeps going offline.
Subfolders
To group commands (commands/fun/, commands/mod/), loop over the subfolders and read the .js files inside each one the same way.
Deploying
Upload the whole commands folder along with index.js using the file manager. File names are case-sensitive on the server, so keep them lowercase to avoid surprises. See deploy a discord.js bot to Schokr.
Events in files too
The same pattern works for events: an events folder where each file exports name, once and execute, loaded with client.on or client.once. The discord.js guide has a full example.