Tutorial

Build a Discord bot with discord.js v14 and slash commands

A small, modern Discord bot: slash commands only, minimal permissions, clean shutdown.

Tutorial3 min readUpdated

Developer Portalapp + bot tokentokenYour botdiscord.jsGateway (WebSocket)interactionCreateDiscordyour serverregister.jsPUT slash commands1run once per change
Figure 1. The bot keeps a gateway WebSocket open; slash commands are registered once over REST.
  1. Create the application and bot

    In the Developer Portal choose New Application. Copy the Application ID from General Information. In the Bot tab press Reset Token and copy the token once; Discord will not show it again.

  2. Invite the bot to your test server

    In OAuth2, use the URL generator with the scopes bot and applications.commands and only the permissions you need (for this bot, Send Messages). Open the URL and pick your test server.

    Enable Developer Mode in Discord (Settings, Advanced) and right-click the server to copy its ID.

  3. Create the project

    mkdir mi-bot-discord && cd mi-bot-discord
    npm init -y
    npm pkg set type=module
    npm install discord.js
    printf 'DISCORD_TOKEN=\nCLIENT_ID=\nGUILD_ID=\n' > .env
    printf '.env\nnode_modules\n' > .gitignore

    Fill .env with the token, the Application ID and the server ID.

  4. Register the slash commands

    register.js

    import { REST, Routes, SlashCommandBuilder } from "discord.js";
    
    const { DISCORD_TOKEN, CLIENT_ID, GUILD_ID } = process.env;
    const commands = [
      new SlashCommandBuilder().setName("ping").setDescription("Check that the bot is alive"),
      new SlashCommandBuilder().setName("echo").setDescription("Repeat a text")
        .addStringOption((o) => o.setName("text").setDescription("What to repeat").setRequired(true).setMaxLength(500)),
    ].map((c) => c.toJSON());
    
    const rest = new REST().setToken(DISCORD_TOKEN);
    // Guild commands update instantly; global commands can take longer to appear.
    const route = GUILD_ID ? Routes.applicationGuildCommands(CLIENT_ID, GUILD_ID) : Routes.applicationCommands(CLIENT_ID);
    const data = await rest.put(route, { body: commands });
    console.log(`registered ${data.length} commands${GUILD_ID ? " in guild " + GUILD_ID : " globally"}`);
    node --env-file=.env register.js
  5. Answer the commands

    index.js

    import { Client, Events, GatewayIntentBits, MessageFlags } from "discord.js";
    
    // Slash commands only need the Guilds intent: no privileged intents required.
    const client = new Client({ intents: [GatewayIntentBits.Guilds] });
    
    client.once(Events.ClientReady, (c) => console.log(`logged in as ${c.user.tag}`));
    
    client.on(Events.InteractionCreate, async (interaction) => {
      if (!interaction.isChatInputCommand()) return;
      try {
        if (interaction.commandName === "ping") {
          await interaction.reply(`pong (${client.ws.ping} ms)`);
        } else if (interaction.commandName === "echo") {
          const text = interaction.options.getString("text", true);
          // allowedMentions stops the bot from pinging @everyone on behalf of a user
          await interaction.reply({ content: text, allowedMentions: { parse: [] } });
        }
      } catch (err) {
        console.error("interaction failed:", err.message);
        if (!interaction.replied) await interaction.reply({ content: "Something went wrong.", flags: MessageFlags.Ephemeral }).catch(() => {});
      }
    });
    
    process.once("SIGTERM", () => client.destroy().finally(() => process.exit(0)));
    await client.login(process.env.DISCORD_TOKEN);

    You must reply to an interaction within 3 seconds. For slow work call interaction.deferReply() first and edit the reply later.

  6. Run and test

    node --env-file=.env index.js
    ~/mi-bot-discord$ node --env-file=.env register.jsregistered 2 commands in guild 1234567890$ node --env-file=.env index.jslogged in as MiBot#12341instant in a test server2bot shows as online
    Figure 2. Expected output (illustration).

    Type /ping in your server. The bot only stays online while the process runs, so deploy it to an always-on host next.

Troubleshooting

  • Commands do not appear: check that the invite included applications.commands and that GUILD_ID is the right server. Global commands (no GUILD_ID) can take a while.
  • Used disallowed intents: you requested a privileged intent (Message Content, Members, Presence) that is not enabled in the Bot tab. Slash-command bots do not need them.
  • The interaction failed: the reply took longer than 3 seconds or threw an error. Check the console and use deferReply for slow work.

Where to run it 24/7: best free hosting for Discord bots, Railway tutorial, VPS with systemd. Official docs: discord.js guide, application commands.