Building a Discord slash command is the first step toward writing a modern Discord bot. The old message-based commands (like !ping) have been replaced by Discord's native slash command interface: when a user types /, commands and their options are listed automatically with validated input. This is now the official way to do it. In this tutorial we'll build a working slash command bot from scratch with discord.js v14 and Node.js 18+: project setup, defining commands with SlashCommandBuilder, deploying via the REST API, and responding through the interactionCreate event.
Project setup and dependencies
Node.js 18 or newer is required; discord.js v14 does not support older versions. Initialize the project in an empty folder and install the single dependency, the discord.js package:
mkdir discord-bot && cd discord-bot
npm init -y
npm install discord.js
We'll use CommonJS rather than ESM, so no extra configuration is needed in package.json; if you prefer modern import syntax you can add "type": "module". This guide uses require-based (CommonJS) JavaScript. Never hardcode secrets like your token and IDs; use a config.json file or environment variables:
{
"token": "YOUR_BOT_TOKEN",
"clientId": "APPLICATION_ID",
"guildId": "TEST_SERVER_ID"
}
- token: taken from the Discord Developer Portal → Bot tab.
- clientId: your Application's ID.
- guildId: the ID of your test server (enable Developer Mode, then right-click the server to copy it).
Defining commands with SlashCommandBuilder
Slash commands are defined with the SlashCommandBuilder class. This class is part of discord.js (formerly @discordjs/builders) and lets you set a command's name, description, and options in a type-safe way. Let's define a simple ping and a greet command that takes a parameter. Putting each command in its own file inside a commands folder is good practice, but for clarity we'll use a single array here:
const { SlashCommandBuilder } = require('discord.js');
const commands = [
new SlashCommandBuilder()
.setName('ping')
.setDescription('Measures the bot latency'),
new SlashCommandBuilder()
.setName('greet')
.setDescription('Greets a user')
.addUserOption(option =>
option
.setName('user')
.setDescription('The person to greet')
.setRequired(true)
),
].map(command => command.toJSON());
module.exports = { commands };
You can add parameters with methods like .addUserOption, .addStringOption, and .addIntegerOption. setRequired(true) makes an option mandatory. The final toJSON() call converts the builder object into the raw JSON structure the Discord API expects.
Deploying commands with REST
Defining commands isn't enough; you have to register them with Discord. For that we write a separate deploy script using the REST client and the Routes helpers. During development use Routes.applicationGuildCommands: it registers to a single server instantly. Global commands (Routes.applicationCommands) appear on every server but can take up to an hour to propagate.
const { REST, Routes } = require('discord.js');
const { token, clientId, guildId } = require('./config.json');
const { commands } = require('./commands');
const rest = new REST({ version: '10' }).setToken(token);
(async () => {
try {
console.log(`Deploying ${commands.length} commands...`);
const data = await rest.put(
Routes.applicationGuildCommands(clientId, guildId),
{ body: commands },
);
console.log(`Successfully registered ${data.length} commands.`);
} catch (error) {
console.error(error);
}
})();
Run this file once with node deploy-commands.js. You must run it again every time you change your commands. The rest.put method replaces (overwrites) the entire set of existing commands with the list you send, so any command you remove from the list gets deleted.
Client setup and the interactionCreate event
The bot itself lives in a separate file (index.js). When creating the Client object you must declare which events you want to receive using GatewayIntentBits. For slash commands only the Guilds intent is needed; you don't need to read message content. We respond to commands in the interactionCreate event and verify that the incoming interaction really is a command with interaction.isChatInputCommand():
const { Client, GatewayIntentBits } = require('discord.js');
const { token } = require('./config.json');
const client = new Client({
intents: [GatewayIntentBits.Guilds],
});
client.once('clientReady', () => {
console.log(`Logged in as ${client.user.tag}`);
});
client.on('interactionCreate', async (interaction) => {
if (!interaction.isChatInputCommand()) return;
if (interaction.commandName === 'ping') {
await interaction.reply(`Pong! Latency: ${client.ws.ping}ms`);
}
if (interaction.commandName === 'greet') {
const user = interaction.options.getUser('user');
await interaction.reply(`Hello ${user}! Welcome.`);
}
});
client.login(token);
Key points to note here:
- isChatInputCommand(): only handles slash commands, not button or menu interactions. Without this check the code may throw when reading
commandName. - interaction.options.getUser('user'): retrieves the value of the option you defined. Use
getString,getInteger, andgetBooleanfor those types. - interaction.reply(): you must respond to an interaction within 3 seconds. For long-running work, call
interaction.deferReply()first and then send the result withinteraction.editReply().
To run the bot, deploy the commands first, then start the client:
node deploy-commands.js
node index.js
Common mistakes
- "Missing Access" error: if you didn't invite the bot with the
applications.commandsscope, the commands won't register. In the OAuth2 URL, select both thebotandapplications.commandsscopes. - Command not showing: guild commands appear instantly, global commands propagate with a delay. Always use guild commands during development.
- "Unknown interaction": the reply arrived later than 3 seconds. Use
deferReply().
Frequently Asked Questions
What's the difference between a slash command and an old prefix command?
Prefix commands (!command) require reading message content and need Discord's privileged MessageContent intent. Slash commands are integrated into the Discord UI, provide autocomplete and input validation, and don't require the message content permission. For new bots, slash commands are the mandatory choice.
Why don't global commands appear immediately?
Discord caches global commands while distributing them to all servers, and that propagation can take up to an hour. For development and testing, register to a single server with Routes.applicationGuildCommands, because guild commands activate instantly.
Which Node.js version does discord.js v14 require?
discord.js v14 requires Node.js 18 or newer. On older versions you'll get an incompatibility error during installation. Check your version with node -v.
Want to take your bot to the next level? If you need help with subcommands, autocomplete, buttons, and a custom bot architecture, get in touch with me and let's bring your project to life together.