NewSchokr 13 is here. A whole new UI, seven locations, snapshots and more. Read more →

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 to index.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_ID set 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.