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
npm install discord.js @discordjs/rest discord-api-typesBot 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:
node deploy-commands.jsHandling 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 commandsclient.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 interactionsclient.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
- Use ephemeral replies for errors and private info
- Defer replies for long-running operations
- Validate permissions in both command definition and execution
- Handle errors gracefully with try-catch
- Use guild commands for testing (instant updates)
- 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.commandsscope
“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.commandsscope is enabled