Discord Bots

Build a Discord Bot with Node.js and Slash Commands

Tech Setup5 min read
TS

Tech Setup

Published August 20, 2026 · Editorial policy

Build a Discord Bot with Node.js and Slash Commands

You have a custom workflow or a developer utility running locally, and you want to trigger it directly from a Discord chat using a native slash command rather than switching contexts to a terminal. Old-school message-prefix bots (!ping) are deprecated by Discord's modern API standards, leaving you stuck trying to parse modern interactions, application IDs, and REST endpoints. By the end of this guide, you will have a standalone Node.js application running with discord.js that registers a global slash command and responds instantly with ephemeral or public messages.

TL;DR: The Quick Boot

If you just want the working code and don't care about the boilerplate walkthrough, clone a fresh directory, initialize your project, and install the required library:

mkdir discord-slash-bot && cd discord-slash-bot
npm init -y
npm install discord.js dotenv

Create a .env file containing your bot credentials:

DISCORD_TOKEN=your_bot_token_here
CLIENT_ID=your_application_id_here

Run your registration script, start your listener, and type /ping in your server to get a Pong! response.

Setting Up Your Discord Application

Before writing any code, Discord needs to know your bot exists and what permissions it holds.

Navigate to the Discord Developer Portal and click New Application. Name your project (e.g., DevToolsBot) and accept the terms.

Configuring Bot Settings

  1. Click Bot on the left-hand sidebar.
  2. Under the username, click Reset Token and copy the resulting string. Paste this immediately into your .env file as DISCORD_TOKEN. Do not commit this token to version control.
  3. Scroll down to Privileged Gateway Intents. For a basic slash command bot, you do not need Presence Intent or Server Members Intent toggled on. Leave them disabled to maintain a minimal security footprint.
  4. Click OAuth2 on the sidebar, then URL Generator.
  5. Under Scopes, select bot and applications.commands.
  6. Under Bot Permissions, select Send Messages.
  7. Copy the generated URL at the bottom, paste it into your browser, and invite the bot to your development Discord server.

Structuring the Project

Create a clean directory layout to separate command registration from event handling. A flat file works for trivial scripts, but separating concerns keeps your codebase maintainable as you add more commands.

discord-slash-bot/
├── .env
├── package.json
├── deploy-commands.js
└── index.js

Ensure your package.json includes "type": "module" so you can use modern ECMAScript modules (import/export) instead of CommonJS (require).

{
  "name": "discord-slash-bot",
  "version": "1.0.0",
  "type": "module",
  "main": "index.js",
  "scripts": {
    "start": "node index.js",
    "deploy": "node deploy-commands.js"
  },
  "dependencies": {
    "discord.js": "^14.14.1",
    "dotenv": "^16.4.5"
  }
}

Registering Slash Commands

Discord requires you to register your slash commands via their REST API before users can run them. If you register commands globally, they can take up to an hour to propagate across all servers. For development, you can register them to a single guild (server) for instant updates.

Create deploy-commands.js and add the following code:

import { REST, Routes, SlashCommandBuilder } from 'discord.js';
import dotenv from 'dotenv';

dotenv.config();

const commands = [
  new SlashCommandBuilder()
    .setName('ping')
    .setDescription('Replies with Pong and latency information!'),
].map(command => command.toJSON());

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

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

  // To deploy globally, use Routes.applicationCommands(process.env.CLIENT_ID)
  // For instant guild updates, use Routes.applicationGuildCommands(clientId, guildId)
  await rest.put(
    Routes.applicationCommands(process.env.CLIENT_ID),
    { body: commands },
  );

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

To find your CLIENT_ID, go back to the Developer Portal, click General Information, and copy the Application ID. Add it to your .env file:

DISCORD_TOKEN=your_bot_token_here
CLIENT_ID=your_application_id_here

Run the deployment script:

npm run deploy

Expected output in your terminal:

$ npm run deploy
Started refreshing application (/) commands.
Successfully reloaded application (/) commands.

Building the Event Listener

Now that your command is registered with Discord's API, you need a persistent Node.js process to listen for user interactions and send responses.

Create index.js and add the core client initialization and interaction handling logic:

import { Client, GatewayIntentBits } from 'discord.js';
import dotenv from 'dotenv';

dotenv.config();

// Create a new client instance with necessary intents
const client = new Client({ intents: [GatewayIntentBits.Guilds] });

// When the client is ready, run this code (only once)
client.once('ready', () => {
  console.log(`Logged in as ${client.user.tag}!`);
});

// Listen for interactions (slash commands)
client.on('interactionCreate', async interaction => {
  // If the interaction isn't a slash command, ignore it
  if (!interaction.isChatInputCommand()) return;

  const { commandName } = interaction;

  if (commandName === 'ping') {
    const latency = Date.now() - interaction.createdTimestamp;
    
    // Reply to the user. 
    // ephemeral: true means only the command runner can see the response.
    await interaction.reply({ 
      content: `Pong! Latency is ${latency}ms.`, 
      ephemeral: false 
    });
  }
});

// Log in to Discord with your client's token
client.login(process.env.DISCORD_TOKEN);

Start your bot:

npm start

Expected output:

$ npm start
Logged in as DevToolsBot#1234!

Open Discord, go to the server where you invited your bot, type /ping, and press Enter. You will see your bot respond directly to your interaction.

If This Doesn't Work: Common Failure Points

When building Discord bots, misconfigured permissions or stale cache states often cause unexpected behavior. Check these common culprits first:

1. The command doesn't appear in the autocomplete menu

  • The Cause: Global commands can take up to an hour to populate across Discord's edge servers.
  • The Fix: Completely restart your Discord client (Ctrl + R on Windows/Linux, Cmd + R on macOS). If you are testing rapidly, modify your deploy-commands.js to use guild-specific commands instead:
    await rest.put(
      Routes.applicationGuildCommands(process.env.CLIENT_ID, 'YOUR_SERVER_ID'),
      { body: commands },
    );
    
    Guild commands update instantly.

2. InteractionNotReplied or InteractionAlreadyReplied errors

  • The Cause: Discord requires an acknowledgment of a slash command within 3 seconds. If your code performs an async operation (like a slow database query or an external API fetch) without deferring the reply, Discord drops the interaction.
  • The Fix: Call await interaction.deferReply(); immediately inside your interaction handler before executing your slow logic, then use interaction.editReply() to send the final payload.

3. Missing intents crashing the client

  • The Cause: Enabling gateway intents in code that are disabled in the Discord Developer Portal dashboard.
  • The Fix: Ensure that any intent you pass into new Client({ intents: [...] }) is also explicitly toggled on under the Bot tab of your application in the Discord Developer Portal. For this tutorial, only GatewayIntentBits.Guilds is required.

Next Steps and Alternatives

You now have a functional webhook listener capable of taking inputs and returning output. From here, you can extend SlashCommandBuilder to accept options (strings, numbers, boolean flags) to build complex internal tools, such as triggering CI/CD pipelines via GitHub webhooks or querying internal metrics dashboards.

If your bot grows beyond a few simple scripts and you find yourself writing massive switch-case statements inside index.js, look into modular command handlers like @discordjs/builders combined with a file-system reader that dynamically loads command definitions from a commands/ directory. Alternatively, if managing persistent WebSocket gateway connections feels too heavy for simple event-driven architecture, investigate building your bot using Discord's HTTP Interactions Endpoint instead of a persistent Gateway bot.