Discord Bots

How to Build a Discord Bot With Slash Commands in Node.js

Tech Setup4 min read
TS

Tech Setup

Reviewed July 24, 2026

How to Build a Discord Bot With Slash Commands in Node.js

Building a Discord bot in 2024 requires moving away from the old-school messageCreate event listener. With the deprecation of message intent-heavy bots, Discord has pushed developers toward Slash Commands—a superior UI that provides auto-completion, parameter validation, and a standardized interaction flow.

In this guide, we will build a robust Discord bot using discord.js (v14+), Node.js, and the REST API for command registration.

Prerequisites

Before we touch the code, ensure you have the following installed:

  • Node.js (v18.0.0 or higher): We use modern JavaScript features, so keep your environment up to date.
  • A Discord Developer Account: Navigate to the Discord Developer Portal to create an application.
  • Basic CLI familiarity: You should be comfortable navigating your terminal.

Step 1: Initialize the Project

First, create a directory for your project and initialize it with npm.

mkdir discord-bot-slash
cd discord-bot-slash
npm init -y

Now, install the necessary dependencies:

npm install discord.js dotenv
  • discord.js: The wrapper for the Discord API.
  • dotenv: To manage environment variables securely.

Create a .env file in your root directory to store your credentials:

TOKEN=your_bot_token_here
CLIENT_ID=your_application_id_here
GUILD_ID=your_server_id_here

Step 2: Registering Slash Commands

Discord commands are not "pushed" by the bot's runtime code every time it starts. Instead, they are registered via the Discord API. We need a separate script to deploy our commands.

Create a file named deploy-commands.js:

const { REST, Routes } = require('discord.js');
require('dotenv').config();

const commands = [
  {
    name: 'ping',
    description: 'Replies with Pong!',
  },
  {
    name: 'echo',
    description: 'Repeats what you say',
    options: [
      {
        name: 'input',
        description: 'The text to echo',
        type: 3, // Type 3 is STRING
        required: true,
      },
    ],
  },
];

const rest = new REST({ version: '10' }).setToken(process.env.TOKEN);

(async () => {
  try {
    console.log('Started refreshing application (/) commands.');

    await rest.put(
      Routes.applicationGuildCommands(process.env.CLIENT_ID, process.env.GUILD_ID),
      { body: commands },
    );

    console.log('Successfully reloaded application (/) commands.');
  } catch (error) {
    console.error(error);
  }
})();

Run this script once to register the commands to your Discord server:

node deploy-commands.js

Step 3: Building the Bot Logic

Now, let's create index.js, the entry point for your bot. We will initialize the client and listen for interactionCreate events.

const { Client, GatewayIntentBits } = require('discord.js');
require('dotenv').config();

const client = new Client({ intents: [GatewayIntentBits.Guilds] });

client.once('ready', () => {
  console.log(`Logged in as ${client.user.tag}!`);
});

client.on('interactionCreate', async interaction => {
  if (!interaction.isChatInputCommand()) return;

  const { commandName } = interaction;

  if (commandName === 'ping') {
    await interaction.reply('Pong!');
  } else if (commandName === 'echo') {
    const input = interaction.options.getString('input');
    await interaction.reply(`You said: ${input}`);
  }
});

client.login(process.env.TOKEN);

Understanding the Architecture

  • Intents: We only need GatewayIntentBits.Guilds because slash commands do not require reading the message content, significantly reducing your bot's memory footprint.
  • InteractionCreate: This event acts as the gateway for all slash commands, context menus, and component interactions.
  • Interaction Object: The interaction object contains the metadata of the command, including the options passed by the user.

Step 4: Refining the Development Workflow

Hardcoding commands in an array inside deploy-commands.js works for small bots, but it quickly becomes unmanageable. As a professional developer, you should modularize your command structure.

Create a Command Handler

Create a directory named commands. Inside it, create ping.js:

module.exports = {
  data: {
    name: 'ping',
    description: 'Replies with Pong!',
  },
  async execute(interaction) {
    await interaction.reply('Pong!');
  },
};

Update your index.js to dynamically load these files using fs (Node.js file system module):

const fs = require('node:fs');
const path = require('node:path');
const { Collection } = require('discord.js');

client.commands = new Collection();
const commandsPath = path.join(__dirname, 'commands');
const commandFiles = fs.readdirSync(commandsPath).filter(file => file.endsWith('.js'));

for (const file of commandFiles) {
  const filePath = path.join(commandsPath, file);
  const command = require(filePath);
  client.commands.set(command.data.name, command);
}

This pattern ensures that adding a new command is as simple as dropping a new file into the commands/ directory.


Best Practices for Tier-1 Developers

1. Handling Deferrals

Discord interactions expire after 3 seconds. If your bot needs to perform an external API request (e.g., fetching weather or database records), it will time out. Always use await interaction.deferReply() if your processing logic takes longer than a few hundred milliseconds.

await interaction.deferReply();
// Perform heavy task...
await interaction.editReply('Task complete!');

2. Ephemeral Messages

If you want a response to be visible only to the user who triggered the command, use the ephemeral flag. This is ideal for sensitive or private information.

await interaction.reply({ content: 'This is a private message!', ephemeral: true });

3. Error Handling

Do not leave interactionCreate prone to unhandled promise rejections. Wrap your execution logic in try/catch blocks:

try {
  await command.execute(interaction);
} catch (error) {
  console.error(error);
  await interaction.reply({ content: 'There was an error executing this command!', ephemeral: true });
}

4. Use Global Commands Carefully

In the example above, we registered commands to a specific GUILD_ID. This is perfect for development because changes appear instantly. When you are ready to deploy globally, switch your Routes call to applicationCommands(clientId). Note: Global command propagation can take up to an hour.


Why Slash Commands Are the Gold Standard

If you have been building legacy bots, the shift to slash commands provides three distinct technical advantages:

  1. Strict Typing: Users cannot send junk data. Discord validates the options you define (e.g., ensuring a user selects a valid "User" type or a specific integer range) before your code even sees the request.
  2. No Message Parsing: You no longer need to use RegEx to identify triggers (e.g., if (message.content.startsWith('!'))). This reduces complexity and potential bugs in input parsing.
  3. Discovery: When a user types /, Discord provides a searchable autocomplete menu. This makes your bot's features discoverable without requiring a dedicated documentation page for command aliases.

Conclusion

Building a bot with slash commands is the modern way to interface with Discord. By modularizing your code using the Collection pattern and handling interactions gracefully, you ensure your bot is scalable, maintainable, and highly performant.

Next steps? Consider implementing Component Interactions (buttons and select menus) or Autocomplete for your slash command options to take your developer experience to the next level.