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.
Invite the bot to your test server
In OAuth2, use the URL generator with the scopes
botandapplications.commandsand 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.
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' > .gitignoreFill
.envwith the token, the Application ID and the server ID.Register the slash commands
register.jsimport { 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.jsAnswer the commands
index.jsimport { 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.Run and test
node --env-file=.env index.jsFigure 2. Expected output (illustration). Type
/pingin 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.