Skip to content

Discord Bot Commands with Slash Commands

Modern Discord bots use slash commands for a better user experience. This guide shows you how to implement them.

Why Slash Commands?

  • Better UX: Native Discord interface with autocomplete
  • Permissions: Built-in permission handling
  • Discoverability: Users can see available commands
  • Validation: Type checking and required parameters

Setup

Install Dependencies

Terminal window
npm install discord.js @discordjs/rest discord-api-types

Bot Configuration

Make sure your bot has these intents:

const { Client, GatewayIntentBits } = require('discord.js');
const client = new Client({
intents: [
GatewayIntentBits.Guilds,
GatewayIntentBits.GuildMessages,
]
});

Creating Commands

Basic Command

Create 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!');
},
};

Command with Options

Create commands/user.js:

const { SlashCommandBuilder } = require('discord.js');
module.exports = {
data: new SlashCommandBuilder()
.setName('user')
.setDescription('Get info about a user')
.addUserOption(option =>
option
.setName('target')
.setDescription('The user to get info about')
.setRequired(true)
),
async execute(interaction) {
const user = interaction.options.getUser('target');
await interaction.reply(`User: ${user.tag}\nID: ${user.id}`);
},
};

Command with Multiple Options

const { SlashCommandBuilder } = require('discord.js');
module.exports = {
data: new SlashCommandBuilder()
.setName('poll')
.setDescription('Create a poll')
.addStringOption(option =>
option
.setName('question')
.setDescription('The poll question')
.setRequired(true)
)
.addStringOption(option =>
option
.setName('option1')
.setDescription('First option')
.setRequired(true)
)
.addStringOption(option =>
option
.setName('option2')
.setDescription('Second option')
.setRequired(true)
),
async execute(interaction) {
const question = interaction.options.getString('question');
const option1 = interaction.options.getString('option1');
const option2 = interaction.options.getString('option2');
const poll = `📊 **${question}**\n\n1️⃣ ${option1}\n2️⃣ ${option2}`;
const message = await interaction.reply({
content: poll,
fetchReply: true
});
await message.react('1️⃣');
await message.react('2️⃣');
},
};

Registering Commands

Create deploy-commands.js:

const { REST, Routes } = require('discord.js');
const fs = require('fs');
require('dotenv').config();
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());
}
const rest = new REST({ version: '10' })
.setToken(process.env.DISCORD_TOKEN);
(async () => {
try {
console.log(`Registering ${commands.length} commands...`);
// Guild commands (instant update)
await rest.put(
Routes.applicationGuildCommands(
process.env.CLIENT_ID,
process.env.GUILD_ID
),
{ body: commands }
);
// OR Global commands (takes up to 1 hour)
// await rest.put(
// Routes.applicationCommands(process.env.CLIENT_ID),
// { body: commands }
// );
console.log('Successfully registered commands!');
} catch (error) {
console.error(error);
}
})();

Run it:

Terminal window
node deploy-commands.js

Handling Commands

In your main index.js:

const fs = require('fs');
const { Client, Collection, GatewayIntentBits } = require('discord.js');
const client = new Client({
intents: [GatewayIntentBits.Guilds]
});
// Load commands
client.commands = new Collection();
const commandFiles = fs.readdirSync('./commands')
.filter(file => file.endsWith('.js'));
for (const file of commandFiles) {
const command = require(`./commands/${file}`);
client.commands.set(command.data.name, command);
}
// Handle interactions
client.on('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);
if (interaction.replied || interaction.deferred) {
await interaction.followUp({
content: 'Error executing command!',
ephemeral: true
});
} else {
await interaction.reply({
content: 'Error executing command!',
ephemeral: true
});
}
}
});
client.login(process.env.DISCORD_TOKEN);

Advanced Features

Subcommands

const { SlashCommandBuilder } = require('discord.js');
module.exports = {
data: new SlashCommandBuilder()
.setName('config')
.setDescription('Bot configuration')
.addSubcommand(subcommand =>
subcommand
.setName('prefix')
.setDescription('Set the bot prefix')
.addStringOption(option =>
option
.setName('value')
.setDescription('New prefix')
.setRequired(true)
)
)
.addSubcommand(subcommand =>
subcommand
.setName('channel')
.setDescription('Set the log channel')
.addChannelOption(option =>
option
.setName('target')
.setDescription('The channel')
.setRequired(true)
)
),
async execute(interaction) {
const subcommand = interaction.options.getSubcommand();
if (subcommand === 'prefix') {
const prefix = interaction.options.getString('value');
await interaction.reply(`Prefix set to: ${prefix}`);
} else if (subcommand === 'channel') {
const channel = interaction.options.getChannel('target');
await interaction.reply(`Log channel set to: ${channel}`);
}
},
};

Choices (Dropdown)

.addStringOption(option =>
option
.setName('language')
.setDescription('Choose a language')
.setRequired(true)
.addChoices(
{ name: 'JavaScript', value: 'js' },
{ name: 'Python', value: 'py' },
{ name: 'TypeScript', value: 'ts' }
)
)

Permissions

const { SlashCommandBuilder, PermissionFlagsBits } = require('discord.js');
module.exports = {
data: new SlashCommandBuilder()
.setName('ban')
.setDescription('Ban a user')
.setDefaultMemberPermissions(PermissionFlagsBits.BanMembers)
.addUserOption(option =>
option
.setName('target')
.setDescription('User to ban')
.setRequired(true)
),
async execute(interaction) {
// Only users with Ban Members permission can use this
const user = interaction.options.getUser('target');
await interaction.guild.members.ban(user);
await interaction.reply(`Banned ${user.tag}`);
},
};

Best Practices

  1. Use ephemeral replies for errors and private info
  2. Defer replies for long-running operations
  3. Validate permissions in both command definition and execution
  4. Handle errors gracefully with try-catch
  5. Use guild commands for testing (instant updates)
  6. Document your commands with clear descriptions

Troubleshooting

Commands not showing up?

  • Wait up to 1 hour for global commands
  • Use guild commands for instant testing
  • Check bot has applications.commands scope

“Unknown interaction” error?

  • Reply within 3 seconds or defer first
  • Make sure event handler is set up correctly
  • Check command is registered properly

Permission errors?

  • Verify bot has required permissions in server
  • Check role hierarchy for moderation commands
  • Ensure applications.commands scope is enabled