Naar de inhoud
Actuele kortingscode:10% kortingGebruiken bij je bestelling

Je eerste discord.js-bot: een slash-commando stap voor stap

Je schrijft een bot in discord.js 14 met de slash-commando's /ping en /pozdrav, registreert de commando's via REST, leert binnen 3 seconden te antwoorden en start de bot bij StormHost.

  • gevorderd
  • ~30 min
  • Bijgewerkt op 2-10-2026

We schrijven een bot met de bibliotheek discord.js en twee slash-commando's: /ping en /pozdrav (“begroet”) met een parameter. We leggen elke regel uit en starten de bot ten slotte bij StormHost.

Wat je nodig hebt

  • Node.js in de LTS-versie op je computer (download op nodejs.org (opent in een nieuw venster)) en een code-editor, bijvoorbeeld Visual Studio Code
  • Een applicatie in het Discord Developer Portal, een token en de bot uitgenodigd op je testserver met de scope applications.commands (handleiding)
  • Een Discord-bot bij StormHost om de bot continu te laten draaien
  • Ongeveer 30 minuten

Hoe het werkt#

Een bot met slash-commando's bestaat uit drie delen:

  1. Definitie van de commando's – de naam, de beschrijving en de parameters van het commando. Je meldt ze één keer bij Discord aan met een apart script via de REST API (registratie).
  2. De draaiende bot – maakt verbinding met Discord (de gateway) en wacht op gebeurtenissen.
  3. Afhandeling van de interactie – als iemand een commando gebruikt, stuurt Discord de bot een interactie en moet de bot daar binnen 3 seconden op antwoorden. Anders ziet de gebruiker dat de applicatie niet heeft gereageerd.

Het registreren en het draaien van de bot staan los van elkaar: je hoeft de commando's alleen opnieuw te registreren als je hun definitie wijzigt (naam, beschrijving, parameters). Voor wijzigingen in de code van het antwoord is geen registratie nodig.

Het project opzetten#

  1. Maak een map muj-bot aan en open daarin een terminal (in VS Code Ctrl+`).

  2. Maak het project aan en installeer de bibliotheken:

    bash
    npm init -y
    npm install discord.js dotenv
    • npm init -y – maakt package.json aan met standaardwaarden (-y = beantwoordt alle vragen met “ja”).
    • npm install discord.js dotenv – installeert discord.js en dotenv (voor het laden van het bestand .env) en zet ze in dependencies.
  3. Stel in package.json "main": "index.js" en "type": "commonjs" in. De voorbeelden gebruiken require(). Wat de velden betekenen, lees je in de handleiding de bot uploaden en starten.

  4. Maak het bestand .env aan:

    .envini
    DISCORD_TOKEN=<BOT_TOKEN>
    CLIENT_ID=<APPLICATION_ID>
    GUILD_ID=<SERVER_ID>

    Vervang <BOT_TOKEN> door het token van je bot, <APPLICATION_ID> door het Application ID van de pagina General Information in het Developer Portal en <SERVER_ID> door het ID van je Discord-testserver. Waar je ze vindt, staat in de handleiding token en intents.

  5. Maak een .gitignore aan met de twee regels node_modules en .env.

De uiteindelijke structuur:

text
muj-bot/
├── .env
├── .gitignore
├── package.json
├── package-lock.json
├── index.js
├── deploy-commands.js
└── commands/
    └── utility/
        ├── ping.js
        └── pozdrav.js

Commando's#

Elk commando heeft een eigen bestand met twee delen: data (de definitie voor Discord) en execute (wat er gebeurt als het commando wordt gebruikt).

commands/utility/ping.jsJavaScript
const { SlashCommandBuilder, MessageFlags } = require('discord.js');

module.exports = {
  data: new SlashCommandBuilder()
    .setName('ping')
    .setDescription('Antwoordt met Pong! en toont de reactietijd van de bot.'),

  async execute(interaction) {
    await interaction.reply({
      content: `Pong! Reactietijd: ${interaction.client.ws.ping} ms`,
      flags: MessageFlags.Ephemeral,
    });
  },
};
RegelWat die doet
require('discord.js')Laadt uit de bibliotheek de klasse SlashCommandBuilder (bouwt de definitie van het commando op) en MessageFlags (berichtvlaggen).
.setName('ping')De naam van het commando, die de gebruiker na de slash typt. 1–⁠32 tekens, alleen kleine letters, cijfers, - en _, zonder spaties.
.setDescription('…')De beschrijving in het commandomenu. Verplicht, 1–⁠100 tekens.
async execute(interaction)De functie die de bot aanroept wanneer het commando wordt gebruikt. interaction bevat alles over dat gebruik (wie, waar, parameters).
interaction.reply({ … })Het antwoord op het commando. Het moet binnen 3 seconden komen.
interaction.client.ws.pingDe reactietijd van de verbinding van de bot met Discord in milliseconden. Direct na het starten kan dit -1 zijn, tot de waarde is gemeten.
flags: MessageFlags.EphemeralAlleen wie het commando heeft gebruikt, ziet het antwoord. Anderen in het kanaal zien het niet.

Een commando met een parameter:

commands/utility/pozdrav.jsJavaScript
const { SlashCommandBuilder } = require('discord.js');

module.exports = {
  data: new SlashCommandBuilder()
    .setName('pozdrav')
    .setDescription('Begroet de opgegeven persoon.')
    .addStringOption((option) =>
      option.setName('jmeno').setDescription('Wie je wilt begroeten').setRequired(true),
    ),

  async execute(interaction) {
    const jmeno = interaction.options.getString('jmeno');
    await interaction.reply(`Hoi, ${jmeno}! Groeten van ${interaction.user.username}.`);
  },
};
  • .addStringOption(…) – voegt een tekstparameter toe (jmeno = naam). Discord biedt die de gebruiker aan als het veld jmeno:.
  • .setRequired(true) – de parameter is verplicht, zonder kan het commando niet worden verstuurd.
  • interaction.options.getString('jmeno') – leest de waarde van de parameter.
  • interaction.user.username – de gebruikersnaam van wie het commando heeft gebruikt.
  • interaction.reply('…') – een openbaar antwoord in het kanaal.

Andere parametertypen: addIntegerOption (geheel getal), addBooleanOption (ja/nee), addUserOption (serverlid), addChannelOption (kanaal), addRoleOption (rol).

Commando's registreren#

Het script deploy-commands.js laadt de definities van alle commando's en stuurt ze naar Discord. Je start het handmatig, alleen wanneer je een commando toevoegt of de definitie ervan wijzigt.

deploy-commands.jsJavaScript
const fs = require('node:fs');
const path = require('node:path');
require('dotenv').config({ path: path.join(__dirname, '.env'), quiet: true });
const { REST, Routes } = require('discord.js');

const commands = [];
const foldersPath = path.join(__dirname, 'commands');
for (const folder of fs.readdirSync(foldersPath)) {
  const commandsPath = path.join(foldersPath, folder);
  for (const file of fs.readdirSync(commandsPath).filter((f) => f.endsWith('.js'))) {
    const command = require(path.join(commandsPath, file));
    if ('data' in command && 'execute' in command) {
      commands.push(command.data.toJSON());
    } else {
      console.log(`[WARNING] Het bestand ${file} heeft geen "data" of "execute".`);
    }
  }
}

const rest = new REST().setToken(process.env.DISCORD_TOKEN);

(async () => {
  try {
    console.log(`Commando's registreren: ${commands.length}`);
    const data = await rest.put(
      Routes.applicationGuildCommands(process.env.CLIENT_ID, process.env.GUILD_ID),
      { body: commands },
    );
    console.log(`Klaar, geregistreerde commando's: ${data.length}`);
  } catch (error) {
    console.error(error);
  }
})();
OnderdeelWat het doet
fs.readdirSync(foldersPath)Loopt de submappen van commands langs (bijvoorbeeld utility) en daarin de .js-bestanden.
command.data.toJSON()Zet de definitie van het commando om naar de vorm die de Discord API verwacht.
new REST().setToken(…)Een client voor de REST API van Discord, aangemeld met het token van de bot. Hij maakt geen verbinding als bot, maar verstuurt alleen het verzoek.
Routes.applicationGuildCommands(CLIENT_ID, GUILD_ID)Het adres voor de commando's van één server (guild).
rest.put(…, { body: commands })Vervangt alle commando's op de server door deze lijst. Een commando dat in de lijst ontbreekt, verdwijnt uit Discord.

Start het op je computer in de projectmap:

bash
node deploy-commands.js
Uitvoertext
Commando's registreren: 2
Klaar, geregistreerde commando's: 2

Servercommando's of globale commando's?#

SoortRouteWaar ze werkenWanneer gebruiken
Server (guild)Routes.applicationGuildCommands(CLIENT_ID, GUILD_ID)Alleen op de server GUILD_ID.Ontwikkelen en testen, een bot voor één server.
GlobaalRoutes.applicationCommands(CLIENT_ID)Op alle servers waar de bot is uitgenodigd met de scope applications.commands, en standaard ook in privéberichten.Een afgeronde bot voor meerdere servers.

Als je hetzelfde commando zowel voor de server als globaal hebt geregistreerd, zie je het op de testserver twee keer. Verwijder de servercommando's voordat je overstapt op globale registratie: start de serverregistratie met een lege lijst (body: []).

Discord beperkt het aanmaken van commando's tot 200 per dag per server. Start het registratiescript daarom niet bij elke start van de bot.

Het hoofdbestand van de bot#

index.jsJavaScript
const fs = require('node:fs');
const path = require('node:path');
require('dotenv').config({ path: path.join(__dirname, '.env'), quiet: true });
const { Client, Collection, Events, GatewayIntentBits, MessageFlags } = require('discord.js');

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

// Commando's laden uit de map commands
client.commands = new Collection();
const foldersPath = path.join(__dirname, 'commands');
for (const folder of fs.readdirSync(foldersPath)) {
  const commandsPath = path.join(foldersPath, folder);
  for (const file of fs.readdirSync(commandsPath).filter((f) => f.endsWith('.js'))) {
    const command = require(path.join(commandsPath, file));
    if ('data' in command && 'execute' in command) {
      client.commands.set(command.data.name, command);
    }
  }
}

client.once(Events.ClientReady, (readyClient) => {
  console.log(`Ready! Logged in as ${readyClient.user.tag}`);
});

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

  const command = interaction.client.commands.get(interaction.commandName);
  if (!command) {
    console.error(`Het commando ${interaction.commandName} bestaat niet.`);
    return;
  }

  try {
    await command.execute(interaction);
  } catch (error) {
    console.error(error);
    const odpoved = { content: 'Er is een fout opgetreden bij het uitvoeren van het commando.', flags: MessageFlags.Ephemeral };
    if (interaction.replied || interaction.deferred) {
      await interaction.followUp(odpoved);
    } else {
      await interaction.reply(odpoved);
    }
  }
});

client.login(process.env.DISCORD_TOKEN);
OnderdeelWat het doet
new Client({ intents: [GatewayIntentBits.Guilds] })Maakt de bot aan. De intent Guilds is genoeg voor slash-commando's en is niet geprivilegieerd. Voor het lezen van gewone berichten zouden GuildMessages en MessageContent nodig zijn.
client.commands = new Collection()De lijst met commando's op naam. Collection is een uitgebreide Map uit discord.js.
client.commands.set(command.data.name, command)Slaat het commando op onder zijn naam, bijvoorbeeld ping.
client.once(Events.ClientReady, …)Wordt één keer uitgevoerd na een geslaagde login. readyClient.user.tag is de naam van de bot.
client.on(Events.InteractionCreate, …)Wordt bij elke interactie uitgevoerd (commando, knop, formulier).
interaction.isChatInputCommand()Laat alleen slash-commando's door en negeert andere interacties.
client.commands.get(interaction.commandName)Zoekt het bestand van het commando op aan de hand van de naam die de gebruiker heeft gebruikt.
try { … } catch (error) { … }Een fout in een commando mag de bot niet laten crashen. De gebruiker krijgt een foutmelding als antwoord en de fout wordt naar de console geschreven.
interaction.replied / interaction.deferredAls de bot al heeft geantwoord of het antwoord heeft uitgesteld, moet hij een volgend bericht via followUp sturen.
client.login(process.env.DISCORD_TOKEN)Logt de bot in met het token uit .env.

Probeer de bot uit op je computer:

bash
node index.js
Uitvoertext
Ready! Logged in as MujBot#1234

Typ op Discord /ping en kies het commando van je bot.

Antwoorden op een commando dat langer duurt#

Als een commando meer dan 3 seconden nodig heeft (een databasequery, een aanroep van een andere API), stel je het antwoord eerst uit en stuur je het resultaat later:

JavaScript
async execute(interaction) {
  await interaction.deferReply();
  const vysledek = await nejakaDlouhaPrace();
  await interaction.editReply(`Klaar: ${vysledek}`);
}
  • interaction.deferReply() – bevestigt de ontvangst meteen. De gebruiker ziet “de bot denkt na”. Gebruik voor een antwoord dat alleen de gebruiker zelf ziet deferReply({ flags: MessageFlags.Ephemeral }).
  • interaction.editReply(…) – vervangt “denkt na” door het echte antwoord. Een uitgesteld antwoord moet je binnen 15 minuten versturen.
  • nejakaDlouhaPrace() – je eigen functie, de naam is een voorbeeld.

Antwoorden met een embed#

Een embed is een bericht met een gekleurde streep, een titel en velden. Handig om informatie overzichtelijk te tonen. Het commando /server toont gegevens over de server:

commands/utility/server.jsJavaScript
const { SlashCommandBuilder, EmbedBuilder, InteractionContextType } = require('discord.js');

module.exports = {
  data: new SlashCommandBuilder()
    .setName('server')
    .setDescription('Informatie over deze Discord-server.')
    .setContexts(InteractionContextType.Guild),

  async execute(interaction) {
    const guild = interaction.guild;
    const embed = new EmbedBuilder()
      .setColor(0xe11d48)
      .setTitle(guild.name)
      .addFields(
        { name: 'Leden', value: String(guild.memberCount), inline: true },
        { name: 'Aangemaakt', value: guild.createdAt.toLocaleDateString('nl-NL'), inline: true },
      )
      .setTimestamp();
    await interaction.reply({ embeds: [embed] });
  },
};
OnderdeelWat het doet
.setContexts(InteractionContextType.Guild)Het commando kan alleen op een server worden gebruikt, niet in privéberichten (daar zou interaction.guild niet bestaan).
new EmbedBuilder()Begint met het opbouwen van de embed.
.setColor(0xe11d48)De kleur van de streep als hexadecimaal getal (hier rood).
.setTitle(guild.name)De titel, hier de naam van de server.
.addFields({ name, value, inline })Velden met een label en een waarde. value moet tekst zijn, vandaar String(…). inline: true zet de velden naast elkaar.
guild.memberCountHet aantal leden van de server. Werkt met de intent Guilds, zonder geprivilegieerde intent.
.setTimestamp()Voegt de huidige tijd toe aan de voettekst.
interaction.reply({ embeds: [embed] })Verstuurt de embed. In één bericht kun je meerdere embeds in de array zetten.

Start na het toevoegen van het bestand opnieuw node deploy-commands.js, het nieuwe commando moet worden geregistreerd.

Wie een commando mag gebruiken#

De standaardrechten stel je direct in de definitie van het commando in:

commands/moderace/smazat.jsJavaScript
const { SlashCommandBuilder, PermissionFlagsBits, InteractionContextType, MessageFlags } = require('discord.js');

module.exports = {
  data: new SlashCommandBuilder()
    .setName('smazat')
    .setDescription('Verwijdert de laatste berichten in het kanaal.')
    .addIntegerOption((o) =>
      o.setName('pocet').setDescription('Hoeveel berichten (1–100)').setMinValue(1).setMaxValue(100).setRequired(true),
    )
    .setDefaultMemberPermissions(PermissionFlagsBits.ManageMessages)
    .setContexts(InteractionContextType.Guild),

  async execute(interaction) {
    const pocet = interaction.options.getInteger('pocet');
    const smazane = await interaction.channel.bulkDelete(pocet, true);
    await interaction.reply({ content: `Verwijderde berichten: ${smazane.size}`, flags: MessageFlags.Ephemeral });
  },
};
  • .setDefaultMemberPermissions(PermissionFlagsBits.ManageMessages) – standaard zien alleen leden met het recht Berichten beheren het commando. Een beheerder van de server kan dat in de serverinstellingen (Integraties) aanpassen.
  • .setMinValue(1) en .setMaxValue(100) – Discord staat geen getal buiten het bereik toe.
  • interaction.channel.bulkDelete(pocet, true) – verwijdert berichten in bulk. true slaat berichten ouder dan 14 dagen over, want die kunnen niet in bulk worden verwijderd.
  • De bot zelf heeft in het kanaal het recht Manage Messages nodig, anders eindigt het commando met DiscordAPIError[50013]: Missing Permissions.

Leden met het recht Administrator worden door geen enkele instelling van het commando tegengehouden. Registreer commando's die alleen voor jou bedoeld zijn (bijvoorbeeld eval) daarom alleen op een besloten testserver.

Gebeurtenis: een nieuw lid verwelkomen#

Naast commando's reageert de bot op gebeurtenissen. Voor het verwelkomen van een nieuw lid is de geprivilegieerde intent Server Members Intent nodig: zet die aan in het Developer Portal op de pagina Bot en voeg hem toe aan de code.

events/guildMemberAdd.jsJavaScript
const { Events } = require('discord.js');

module.exports = {
  name: Events.GuildMemberAdd,
  once: false,
  async execute(member) {
    const kanal = member.guild.systemChannel;
    if (!kanal) return;
    await kanal.send(`Welkom op de server, ${member}!`);
  },
};

Pas in index.js de intents aan en voeg het laden van de gebeurtenissen toe (na het laden van de commando's):

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

// … commando's laden …

const eventsPath = path.join(__dirname, 'events');
for (const file of fs.readdirSync(eventsPath).filter((f) => f.endsWith('.js'))) {
  const event = require(path.join(eventsPath, file));
  if (event.once) {
    client.once(event.name, (...args) => event.execute(...args));
  } else {
    client.on(event.name, (...args) => event.execute(...args));
  }
}
  • GatewayIntentBits.GuildMembers – Discord stuurt dan gebeurtenissen wanneer leden binnenkomen en vertrekken. Als de intent niet is aangezet in het Developer Portal, meldt de bot Used disallowed intents.
  • member.guild.systemChannel – het systeemkanaal van de server (waar Discord welkomstberichten plaatst). Als de server er geen heeft, stuurt de bot niets.
  • ${member} – voegt een vermelding van het nieuwe lid in (@naam).
  • client.once / client.on – een gebeurtenis met once: true wordt maar één keer verwerkt (start van de bot), de andere elke keer.

Structuur van een grotere bot#

  • Commando's per categorie in de mappen commands/moderace, commands/zabava … De laadcode hierboven loopt ze allemaal langs.
  • Gebeurtenissen in de map events, elk in een eigen bestand met name, once en execute (zie hierboven). Ook de afhandeling van ClientReady en InteractionCreate kun je naar events verplaatsen.
  • Configuratie (ID's van kanalen en rollen) in config.json, geheime gegevens in .env.
  • Gegevens (punten, waarschuwingen) in een database, niet in variabelen in het geheugen, want die gaan bij een herstart verloren.

Starten bij StormHost#

  1. Voer de registratie van de commando's (node deploy-commands.js) uit op je computer. Op de server is dat niet nodig.
  2. Stop de bot op je computer (Ctrl+C). Twee draaiende kopieën met hetzelfde token stelen elkaars interacties.
  3. Upload index.js, package.json, package-lock.json en de map commands met alle submappen op het tabblad Bestanden. Heb je het verwelkomen van een nieuw lid toegevoegd, upload dan ook de map events. Zonder die map crasht de bot bij het starten met de fout ENOENT: no such file or directory. Maak .env direct op de server aan. Zo doe je dat: de bot uploaden en starten.
  4. Controleer op het tabblad Instellingen of het hoofdbestand index.js is.
  5. Klik op het tabblad Console op Starten.

Controle#

  • In de Console staat Ready! Logged in as … en de bot is online op Discord.
  • Als je op de testserver / typt, bevat het menu ping en pozdrav met het pictogram van je bot.
  • /ping antwoordt met een bericht dat alleen jij ziet, /pozdrav jmeno:Jan met een openbaar bericht.

Als iets niet werkt#

De commando's verschijnen niet in het menu#

  • De registratie is niet uitgevoerd: start node deploy-commands.js en controleer de uitvoer Klaar, geregistreerde commando's: ….
  • GUILD_ID is het ID van een andere server dan die waarop je test.
  • De bot is uitgenodigd zonder de scope applications.commands. Nodig hem opnieuw uit met een link met zowel bot als applications.commands.
  • Discord onthoudt het menu. Herstart de Discord-app (Ctrl+R).

De registratie eindigt met DiscordAPIError[50001]: Missing Access#

De bot zit niet op de server GUILD_ID, of heeft daar de scope applications.commands niet. Controleer het ID van de server en nodig de bot opnieuw uit.

Discord meldt “The application did not respond” (de applicatie reageerde niet)#

De bot heeft niet binnen 3 seconden geantwoord. Of hij draait niet (controleer de Console), of het commando duurt lang en deferReply() ontbreekt, of er is in het commando een fout opgetreden vóór het antwoord (die vind je in de Console).

DiscordAPIError[10062]: Unknown interaction#

Het antwoord kwam te laat (na 3 seconden), of er draaien twee kopieën van de bot met hetzelfde token en de andere heeft de interactie al verwerkt. Gebruik deferReply() en laat maar één kopie draaien.

DiscordAPIError[40060]: Interaction has already been acknowledged#

Je hebt voor één interactie twee keer reply() aangeroepen. Stuur het tweede bericht via followUp(), of gebruik na deferReply() editReply().

[WARNING] Het bestand … heeft geen "data" of "execute".#

Een bestand in de map commands exporteert niet beide delen. Controleer module.exports = { data, execute } en kijk of er typefouten in staan.

require is not defined in ES module scope of Cannot use import statement outside a module#

CommonJS en ES-modules worden door elkaar gebruikt. De voorbeelden zijn voor CommonJS: in package.json moet "type": "commonjs" staan (of het veld type ontbreekt). Meer details vind je in de handleiding logs en fouten.

Samenvatting

  • Slash-commando = definitie (SlashCommandBuilder), registratie (deploy-commands.js via REST) en afhandeling (Events.InteractionCreate).
  • Start de registratie alleen als de definitie verandert. Tijdens het ontwikkelen per server (applicationGuildCommands), een afgeronde bot globaal (applicationCommands).
  • Antwoord binnen 3 seconden op een interactie. Begin langer werk met deferReply() en rond het af met editReply().
  • Vang fouten in commando's af met try/catch, zodat één fout de bot niet laat crashen. Start nooit twee kopieën met hetzelfde token.

Zoeken in de handleidingen