DEV Community

Cover image for How to create a discord bot with javascript
Falcão
Falcão

Posted on

How to create a discord bot with javascript

If you use Discord you probably knows some bot used in the platform, be it Loritta, MEE6 or some other. In fact, using bots in your server is a crucial part of the Discord experience nowadays. With that in mind, let's find out today how to create our own bot!

Table of Contents

Why create a bot?

As I said earlier, nowadays on Discord we have thousands of bots with the most diverse functionalities, from moderation to RPG or something completely new, so why bother creating your own?

There are several reasons that can attract you to the development of bots, the first of them is simply curiosity, maybe you are interested in how they are made, another is the need, maybe you had a crazy idea that no other bot has implemented yet and want to create it (I for example have a bot that is only available on my college class server).

Whatever the reason is, it is going to lead you to study and practice, develop real projects and study unknown areas is the key to becoming a better developer, and maybe who knows, you might fall in love with the area :) ok, enough rambling and let's get our hands dirty!

Configuring the Development Enviroment

First of all, we need to install Node.js and an IDE or text editor, I like Visual Studio Code.

After that, create a new folder with the name you want, your project will be inside this folder, enter your newly created folder with your preferred code editor and use the command npm init -y, this will create a javascript project for you.

Content of the file package.json

As you can see, this command created the file package.json, this file stores the information of your project, as the focus of this article is not to teach javascript, I will not go into the functionality of each field, the important thing to know is that this file is essential for the correct functioning of your project.

Creating a new Application

Before doing anything else in our project, we need to register a new application on the Discord Developer Portal

This is the Discord applications page, here all your applications can be viewed and edited, "applications" is the term Discord uses to refer to bots, if you have never created one before, this page will be empty, let's click on "New Application" in the upper right corner.

Discord Developers Portal Initial Page

You'll now be asked to enter a name for your application, don't worry, it can be edited later!

Application name

After that, the general information page will appear, it looks like this:

General information about the bot

This page contains information such as the name, description, photo, tags, number of servers of your bot among others, feel free to leave the bot with your face, for simplicity, I will leave everything as it is.

The next step is to create an invite link for your bot, so we can add it to any server we want, for that we need to select the OAuth2 button in the side menu, when the menu opens below this button choose the URL generator.

OAuth2 Button

In the "Scopes" section, we will select "bot" and "applications.commands", this will tell Discord that we want to add a Bot that has permission to register commands.

Bot Scopes

As soon as you click on "bot", a new section of bot permissions will appear, this is used for Discord to automatically create a role for your bot when it joins a server, so you make sure that your bot will have certain permissions that may be essential for its operation (if the person who adds the bot allows it), in this tutorial we don't need to select any, because our bot will only send messages, something that the "everyone" role can already do.

Scroll to the bottom of the page and copy the invite link Discord has generated, this is the link that will be used to add your bot to servers, add your bot to a server of yours that serves to do tests (I recommend creating a new server for this).

Invite link

Once again access the lateral menu anc click on the "Bot" page.

Bot page button

On this page we have a very important part, the "Build-A-Bot" section, it controls how users see your bot, your photo, your username and it also stores your token.

The token is what tells Discord which is your bot, do not share it, whoever has access to your token has access to your bot, go ahead and click to reset the token, don't forget to write it down because Discord only shows it once!

Build-A-Bot Section
(don't waste your time trying to control my bot, it has already been reset)

Finally, scroll down to the "Priviled Gateway Intents" section, on the same page, and activate the permission, "Message Content Intent", this section contains special permissions, which if your bot is in more than 100 servers, requires a Discord analysis to be activated, but don't worry, that's not our case. This permission is necessary for the bot to be able to read the messages that users send, so it can respond to them.

Special Permissions Section

If you've made it this far, congratulations! You've successfully registered a new bot on Discord, now let's see how to program it.

Giving your Bot life

Previously we created a new javascript project, let's access it again.

We will use a library called discord.js, it is one of the most used libraries for creating bots and will be our main tool.

We can install it by running the command npm i discord.js, note that a folder called node_modules was created in your project, it stores all the libraries you install, don't mess with it unless you know what you're doing!

Let's now create a new file called .env, who will be responsible with storing our bot token in a safe way, when you create it put "TOKEN=" and your token after it, this way we create an environment variable called TOKEN.

.env file content

To be able to use that variable in our project, we need to download a library called dotenv, npm i dotenv.

Besides that, we need to create a file called index.js, it will be our bot's main file, responsible for keeping it online, registering commands, etc.

Initially the content of our index file will be the following:

// Importing the necessary dependencies
const { Client, GatewayIntentBits } = require("discord.js")
require("dotenv").config()

// Creating the client instance
const client = new Client({ intents: [GatewayIntentBits.Guilds, GatewayIntentBits.GuildMessages, GatewayIntentBits.MessageContent] })

// When the client (bot) is ready, we write to the console
client.once("ready", () => {
    console.log("Hello world!")
})

// Logging the bot
client.login(process.env.TOKEN)

Enter fullscreen mode Exit fullscreen mode

This is a very straightforward code, we import dotenv and the necessary classes from discord.js, create a new "client" that is our bot's client, these GatewayIntentBits are events (intents) that you declare that you want to receive in our case we are interested in events related to messages and servers (guilds), as well as the content of the message, which is that special permission we activated earlier, for more information check the Discord Documentation.

Afterwards, we declare the function client.once("ready"), when the bot is logged in, all the code inside it will be executed only once, note the difference between client.on and client.once, the first one executes the code every time that event happens, while the second one executes only the first time that event happens.

Finally we invoke client.login passing the token we put in .env, if everything is correct, when you use node . in the terminal, the bot should respond and be online on discord.

Bot responding on the terminal
Bot online on discord

Creating your first command

The time that everyone was waiting for has come, let's create our first command! To do this, open your index.js file again and let's add the following code:

client.on("messageCreate", (message) => {
    if (message.content === "!ping") {
        message.reply(`Pong! ${client.ws.ping}ms`)
    }
})
Enter fullscreen mode Exit fullscreen mode

This is the standard structure of what we call "prefix commands", since we use prefixes to differentiate common messages from command calls, in this case the prefix "!", as you can see, we use here the client.on, because as it is a command, we want it to be whenever necessary, and we use the messageCreate, event that happens every time a message is sent.

Inside the event, we check if the message is "!ping", if it is, we respond with "Pong!" and the bot's ping, which is the time it takes for the bot to respond to the message, in milliseconds. If everything is correct, when you type "!ping" in the discord chat, the bot should respond with "Pong!" and the ping:

Bot replying: Pong! 273ms

This is a pretty simple command, but it illustrates very well how these types of commands work, however, in the next topic we will learn about "slash commands", which are much more powerful and easy to use, besides being the way Discord recommends to create commands.

Slash commands

Note that the way we created a command previously, adding a new "if" for each command would be a headache, it would be necessary to chain "ifs" or a huge switch case that would basically encapsulate your entire project, which is terrible for the developer and user.

To mitigate this problem, we will create a folder called src and inside it a folder called commands, this way, each command will be its own file, let's start with a command called user.js.

Project structure, with the user.js file inside the commands folder inside the src folder

A Slash Command is nothing more than an object with the properties "data" and "execute" (of course you can add more properties, but these are the essential ones), "data" refers to the information of your command, such as name, description, arguments, etc. As the name of the file suggests, we will make a command that shows some information about a user, we start like this:

const { SlashCommandBuilder, EmbedBuilder, time } = require("discord.js")

module.exports = {
    data: new SlashCommandBuilder()
        .setName("user")
        // we can internacionalize the name of the command like this, 
        // so if the user is using the discord in portuguese, the command will appear in portuguese
        .setNameLocalization("pt-BR", "usuário")
        .setDescription("See some information about a user")
        .setDescriptionLocalization("pt-BR", "Veja algumas informações sobre um usuário")
        // we don't want this command to be used in DM
        .setDMPermission(false)
        // we declare an argument that the user will have to provide when using the command, of the type user
        .addUserOption((option) =>
            option
                .setName("user")
                .setNameLocalization("pt-BR", "usuário")
                .setDescription("The user to get information")
                .setDescriptionLocalization("pt-BR", "O usuário para pegar as informações")
                .setRequired(true)
        ),
Enter fullscreen mode Exit fullscreen mode

Let's hold on for a moment, it's a lot of new information at once, don't rush and read everything and the comments, most of the methods are trivial and are explained by the name.

Here we are declaring a command using the SlashCommandBuilder, notice that we pass the name and description in English and use the "Localization" methods to put the name in Portuguese, this is a great tool for internationalization of discord, so whoever is using the application in Portuguese will see the command in Portuguese, otherwise, the command will be shown in English and for us developers nothing changes.

We use .setDMPermission(false) to indicate that we don't want this command to be used in DM, additionally, we declare an argument that the user will have to provide when using the command, of the type user.

Now let's write the propert execute below the code we already have:

    execute: async ({ interaction }) => {
        // I always like to do this to have more time to respond, it's that message that the bot is thinking
        await interaction.deferReply().catch(() => {})

        // we get the user that was passed as an argument
        const member = interaction.options.getMember("user")

        // we get user information and put it in an embed
        const embed = new EmbedBuilder()
            .setTitle(member.displayName)
            .setColor(member.displayColor)
            .setThumbnail(member.displayAvatarURL())
            .addFields(
                {
                    name: "Membro do Discord desde",
                    value: time(member.user.createdAt),
                    inline: true,
                },
                {
                    name: "Membro desse servidor desde",
                    value: time(member.joinedAt),
                    inline: true,
                }
            )
            .setFooter({ text: member.user.tag, iconURL: member.displayAvatarURL() })

        // we need to edit the response and not just reply because deferReply() was used earlier
        interaction.editReply({ embeds: [embed] })
    },
}

Enter fullscreen mode Exit fullscreen mode

Like the name implies, this is the piece of code that will be executed every time a user uses this command, in short, we create an embed with some information about the user, such as image, name, when they created their discord account, etc.

If you are like me, maybe when you finish this command you have already restarted the bot and tried to use the command, if you did that, you noticed that it doesn't appear, why?

Handling commands and events

Our command does not appear yet because slash commands are different from prefix commands, this is because we have to register them in the Discord API, let's learn how to do that.

Inside the src folder, but outside the commands folder, we will create a new file functions.js

Project Structure with the functions.js file outside the commands folder but inside the src folder

Inside this file, we will declare the function loadFiles:

const fs = require("fs")
const { promisify } = require("util")

const readdir = promisify(fs.readdir)

async function loadFiles(dirName) {
    const basePath = `${process.cwd().replace(/\\/g, "/")}/src/${dirName}`

    const files = []
    const items = await readdir(basePath)
    for (const item of items) {
        const itemPath = `${basePath}/${item}`
        if (itemPath.endsWith(".js")) {
            files.push(itemPath)
            delete require.cache[require.resolve(itemPath)]
        }
    }

    return files
}
Enter fullscreen mode Exit fullscreen mode

Maybe this code looks alien to you, but it serves to load all the files in a folder inside the src directory.

In sequence, we will write the function loadCommands, which as the name suggests serves to load all the commands and register them:

async function loadCommands(client) {
    await client.commands.clear()

    const commandsArray = []

    const Files = await loadFiles("commands")

    Files.forEach((file) => {
        const command = require(file)
        client.commands.set(command.data.name, command)
        commandsArray.push(command.data.toJSON())

        console.log(`Command: ${command.data.name} ✅`)
    })

    client.application.commands.set(commandsArray)
}
Enter fullscreen mode Exit fullscreen mode

Analagously, we will declare the loadEvents, so we can declare events as we declare commands, one per file inside its own folder:

async function loadEvents(client) {
    await client.events.clear()

    const Files = await loadFiles("events")

    Files.forEach((file) => {
        const event = require(file)

        const execute = (...args) => event.execute(...args, client)
        client.events.set(event.name, execute)

        if (event.once) {
            client.once(event.name, execute)
        } else {
            client.on(event.name, execute)
        }

        console.log(`Events: ${event.name} ✅`)
    })
}

module.exports = {
    loadFiles,
    loadEvents,
    loadCommands,
}
Enter fullscreen mode Exit fullscreen mode

Pay attention that if in the event declaration there is the property once: true, we declare it as client.once and otherwise as client.on, difference that we discussed earlier. Finally, we export the 3 functions.

Maybe now you are a little confused, I just showed you 3 functions and went into little detail about them, I ask for patience, soon the importance of them will become clear.

Now that we hve these functions, let's go back to index.js and change the code:

// Importing the necessary dependencies
const { Client, GatewayIntentBits, Collection } = require("discord.js")
require("dotenv").config()

// Importing the functions we created
const { loadEvents, loadCommands } = require("./src/functions")

// Creating the client instance
const client = new Client({
    intents: [GatewayIntentBits.Guilds],
})

// When the client (bot) is ready, we write to the console
client.once("ready", () => {
    console.log("Hello world!")
})

// Logging the bot
client.login(process.env.TOKEN)
Enter fullscreen mode Exit fullscreen mode

Attentive readers will notice that we imported the functions we just created, more attentive readers will notice that we removed some of the intents that existed before, that's because we don't need to read messages anymore (which also implies that that special permission we activated in the Discord portal is also not necessary in this type of command), and even more attentive readers will notice that we added Collection to the classes we import from discord.

A Collection is a data structure created by discord.js, which is basically a Map with extra methods, we will use it inside the "ready" event as follows:

// When the client (bot) is ready, we write to the console
client.once("ready", () => {
    console.log("Olá mundo!")

    client.commands = new Collection()
    client.events = new Collection()

    loadEvents(client)
    loadCommands(client)
})
Enter fullscreen mode Exit fullscreen mode

The commands and events will be stored inside client using the Collections, we use loadEvents and loadCommands to load them.

Now, you may be wondering why I created a function called loadEvents and called it inside the main file, when we don't have any events, remember how in the prefix commands it was necessary to use messageCreate? So, to control the prefix commands, we also need an event, the interactionCreate.

Let's create the folder events inside the src folder and inside it the file interactions.js

Project Structure with the interactions.js file inside the events foled inside the src folder

We will declare the event interactionCreate inside this file:

module.exports = {
    name: "interactionCreate",
    execute: async (interaction, client) => {
        if (interaction.isChatInputCommand()) {
            // we get the command by the name
            const command = client.commands.get(interaction.commandName)

            // we invoke the command's execute function, passing some useful information
            command.execute({
                interaction,
                client,
                member: interaction.member,
                guild: interaction.guild,
                user: interaction.user,
                channel: interaction.channel,
            })
        }
    },
}
Enter fullscreen mode Exit fullscreen mode

This is the structure of an event, name refers to the type of event, in this case interactionCreate and again we have the execute as the code that will be executed every time this event is invoked.

We use interaction.isChatInputCommand() to make sure it is a command that is invoked by chat, because we have other types of commands that are handled within this same event, after that we check which command it is and invoke its execute passing some useful properties.

Alright! Now, if we restart the bot, we will see the bot in the terminal telling us that the user command and the interactionCreate event were loaded successfully.

Terminal with the success messages

Now, if we go to the Discord and type /user, we will see the command!

Command appearing in discord chat

Choose a user and see the result!

Command working in the discord chat, showing a embed with some information about the choosen user

Congratulations! You have created your first Discord bot!

This is the basic structure of a bot, there are still many more advanced concepts, cooldowns, sub commands, tests... but with this structure you can already start your journey in this universe!

Tips

I will leave here some loose tips for you who want to keep developing in this area:

  • Create a second bot, leave your first bot as a "production" environment and do your tests on the second bot, that way you can develop without affecting your users (we can switch between the two just by changing the token)
  • The site top.gg contains thousands of different bots and it is possible to filter by categories, it is a good place to search for other bots similar to yours and it is also possible to put your bot there for more exposure
  • Think about creating a website for your bot, if you want to verify your bot, you have to fill some requirements, two of them is to provide the link to your Privacy Policy and Terms of Service, a good way to do this is with a website, which is also a good vehicle to promote your bot
  • My way of creating bots is not supreme! The discord.js library is very non-opinionated and there are infinite ways to declare a bot (it is even possible to create bots with Typescript with the same library), use Github! Search!

Learn more

If you wish to learn more and take your bot to the next level, here are some useful materials:

  • Discord.js Guide this is a very complete and well written guide that covers everything I covered here and MUCH more!
  • Discord.js library documentation the documentation is a great site to save and check whenever you need, it is very simple to find what you want inside it and see all the methods and properties of that class
  • A little marketing on my part, but which can also be useful as a reference are the repositories of my main bot, my own discord minigames library and my discord bot template which is a little more complete than what we developed together here
  • The code developed in this article will also be available on my github

Conclusion

If you sticked with me until here, thank you very much! I hope you have at least learned something new, this is my first article here so any (constructive) criticism, comment, question or compliment please leave in the comments :)

Top comments (0)