Create a Book Club Bot using Discord.js v14

by Lucas Minter

← go back to all posts

This tutorial is based off of my course released on egghead.io.

In this tutorial, I will guide you through creating your first Discord bot. We will begin by setting up our bot in the Discord developer portal, followed by setting up the bot locally. I will teach you how to develop a basic chatbot, and then we will dive into creating slash commands with more detail. Finally, we will deploy the bot to ensure it runs continuously.

I have found the following resources to be helpful while developing:

  • Discord.js' guide on their website is an excellent resource for learning more about each topic covered in this tutorial.
  • If you have any questions you can't figure out, I highly encourage you to check out the Discord.js discord channel. However, please try to solve the problem yourself first. Accurately describing the issue and the attempted solutions is a great way to get a quick answer.
  • The Discord status page is a useful tool to check for any Discord outages. If your bot isn't working and you can't find the issue, verifying that everything is running on Discord's end is a great debugging step.

Create and Configure a Bot using the Discord Developer Portal

To get started, go to https://discord.com/developers/applications. If you do not have an account, you will need to sign up for one. Once you're in, click "New Application" and name your bot.

In the navigation column on the left, click "Bot," then click "Add Bot." This will convert your application into a bot.

Next, click on "OAuth2," then "URL Generator," and select "bot" to set the necessary permissions. The specific permissions required will depend on what you want your bot to do. Since we're creating a book club event bot, we need to grant it the following permissions: manage roles, manage channels, read messages/view channels, moderate members, send messages, manage messages, add reactions, and use slash commands. Copy the generated URL and add it to your Discord channel.

Send your first Discord.js Bot Event

To begin, create a directory and navigate to it using the command line. Then, run yarn init -y to generate the initial files. We will also need to install the following dependencies: discord.js, dotenv, and nodemon.

After that, create a .env file to save some variables. In the Discord developer portal, we will need two variables: application id, which will be saved as CLIENT_ID, and a token that can be found by clicking "Reset Token" in the "Bot" section. Save the token in your .env file as TOKEN.

Next, create a main.js file in the root of your directory and import the necessary packages:

const { Client, Intents } = require('discord.js');
require('dotenv').config();

We will then create a new client instance:

const client = new Client({ intents: [Intents.FLAGS.GUILDS] });

Once we have created the client, it is common practice to log in to make sure the bot is working as intended:

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

We can then log in to Discord with our client's token:

client.login(process.env.TOKEN);

Lastly, in package.json, create a development script using nodemon:

"scripts": {
  "dev": "nodemon main.js"
},

Run yarn dev to start your bot!

Create your first Discord.js Bot Message Command

To create your first message command, you need to add the necessary permissions via the GatewayIntentBits. Here is an example of how to set up the client with the required intents:

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

Using client.on, the bot will check the messages sent in the server to see if they match up with the desired command name, and then reply to that command with some text. Here is an example:

client.on('messageCreate', (message) => {
  if(message.content === 'hello'){
    message.reply('world');
  }
});

If you want to create a simple chatbot, you can keep checking the messages in your server with if/else statements and reply accordingly. However, if you want to create a more intricate bot, you'll want to use slash commands.

What are Slash Commands

Using if/else blocks to create a bot can become cluttered and hard to read as it becomes more complex. However, Slash Commands offer a solution by allowing you to separate commands into individual files and providing helpful configurations.

According to the Discord.js tutorial, here are some advantages of using Slash Commands:

  • Integration with the Discord client interface.
  • Automatic command detection and parsing of associated options/arguments.
  • Typed argument inputs for command options, such as "String", "User", or "Role".
  • Validated or dynamic choices for command options.
  • In-channel private responses (ephemeral messages).
  • Pop-up form-style inputs for capturing additional information.

Therefore, if you plan on creating a more complex bot, it is recommended to use Slash Commands to keep your code organized and manageable.

Setup Files for your first Discord.js Slash Command

Thanks to the Discord.js guide, we can use some of their files to set up our project completely for slash commands. While you can have these functions set up in your main.js file, it's much more readable to extract them into their own folder and files.

Here are five files that I grabbed from the Discord.js guide to run slash commands. I won't go into detail here, so you can either go through their documentation or watch my course on egghead.io.

You can manually add the files listed, or you can clone the template I created, fill out the .env file, and be ready for the next section.

Main.js

const fs = require('node:fs')
const path = require('node:path')

const { Client, GatewayIntentBits, Collection } = require('discord.js');
require('dotenv').config()

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

client.commands = new Collection();

const commandsPath = path.join(__dirname, 'commands');
const commandFiles = fs.readdirSync(commandsPath).filter(file => file.endsWith('.js'));

for (const file of commandFiles) {
    const filePath = path.join(commandsPath, file);
    const command = require(filePath);
    if ('data' in command && 'execute' in command) {
        client.commands.set(command.data.name, command);
    } else {
        console.log(`[WARNING] The command at ${filePath} is missing a required "data" or "execute" property.`);
    }
}

const eventsPath = path.join(__dirname, 'events');
const eventFiles = fs.readdirSync(eventsPath).filter(file => file.endsWith('.js'));

for (const file of eventFiles) {
    const filePath = path.join(eventsPath, file);
    const event = require(filePath);
    if (event.once) {
        client.once(event.name, (...args) => event.execute(...args));
    } else {
        client.on(event.name, (...args) => event.execute(...args));
  }
}

client.login(process.env.TOKEN);

events/interactionCreate.js

const { Events } = require('discord.js');

module.exports = {
    name: Events.InteractionCreate,
    async execute(interaction) {
        if (!interaction.isChatInputCommand()) return;

        const command = interaction.client.commands.get(interaction.commandName);

        if (!command) {
            console.error(`No command matching ${interaction.commandName} was found.`);
            return;
        }

        try {
            await command.execute(interaction);
        } catch (error) {
            console.error(`Error executing ${interaction.commandName}`);
            console.error(error);
        }
    },
};

events/ready.js

const { Events } = require('discord.js');

module.exports = {
    name: Events.ClientReady,
    once: true,
    execute(client) {
        console.log(`Ready! Logged in as ${client.user.tag}`);
    },
};

deploy-commands.js

const { REST, Routes } = require('discord.js');
require('dotenv').config()
const fs = require('node:fs');

const commands = [];
// Grab all the command files from the commands directory you created earlier
const commandFiles = fs.readdirSync('./commands').filter(file => file.endsWith('.js'));

// Grab the SlashCommandBuilder#toJSON() output of each command's data for deployment
for (const file of commandFiles) {
    const command = require(`./commands/${file}`);
    commands.push(command.data.toJSON());
}

// Construct and prepare an instance of the REST module
const rest = new REST({ version: '10' }).setToken(process.env.TOKEN);

// and deploy your commands!
(async () => {
    try {
        console.log(`Started refreshing ${commands.length} application (/) commands.`);

        // The put method is used to fully refresh all commands in the guild with the current set
        const data = await rest.put(
            Routes.applicationGuildCommands(process.env.CLIENT_ID, process.env.GUILD_ID),
            { body: commands },
        );

        console.log(`Successfully reloaded ${data.length} application (/) commands.`);
    } catch (error) {
        // And of course, make sure you catch and log any errors!
        console.error(error);
    }
})();

commands/ping.js


const { SlashCommandBuilder } = require('discord.js');

module.exports = {
    data: new SlashCommandBuilder()
        .setName('ping')
        .setDescription('Replies with Pong'),
    async execute(interaction) {
        await interaction.reply(`Pong!`);
    },
};

If you're looking to learn more about building a bot using Discord.js, head on over to egghead.io where I've created a whole video course on the subject!