How to Build a Discord Bot With Node.JS
Damir Ljubičić
Posted on February 7, 2024
In this tutorial, you will learn how to connect a bot to the Discord server and create commands that it will respond to. To achieve this, you will use Node, Typescript, DiscordJS, and dependency injection(InversifyJS). But before we start, let’s talk about the benefits Discord offers.
Every day we have more and more VoIP services to choose from, but most gamers have already decided on Discord for communication. Today, not only gamers but also agencies and companies started using Discord. Why? You may ask because it offers everything that business communication platforms, such as Slack or RocketChat, need. The main differences are that Discord:
- Is free to use,
- Provides out-of-the-box solutions without much setup or configuration,
- It's perfect for larger teams, as it offers text and voice support for a great number of people
Note: Discord, along with all of the features we will be using in this tutorial, is free to use.
Before we begin with the tutorial, I assume you have the basics of Typescript and a beginner level of understanding of NodeJS. It is also recommended you are at least at an intermediate level of Javascript. Since you will be using NodeJS, having an environment set up on your PC will help you follow the code. To finish this tutorial you will also need a Discord account, so if you don’t have one, please create one.
To summarize, in this tutorial, you will learn:
- How to create a Discord server and a bot
- How to add a Discord bot to your server
- How to set up a Node project with Typescript superset
- How to use Dependency Injection with InversifyJS
- How to create business logic for bot commands with DiscordJS
- How to test commands through the Discord channel
Note: If you want to skip ahead to the final project, check out the code in my GitHub repository.
Setting up the project
For the IDE, you can use whatever you feel comfortable with - for me, that is Webstorm. Before creating a bot, you need to create a project. To do that, use the following command:
npm init
This will open an interactive process for initializing your project with the package.json file. Fill these questions or just skip all by pressing Enter, because it does not matter for this tutorial.
After initializing the project, install the following dependencies by running this command inside your project (terminal):
npm i --save discord.js dotenv typescript inversify @types/node reflect-metadata
Then for the last step of setup, you need to change scripts inside the package.json file. Later on, you will need two scripts, and these start and watch.
We will use the start script to start the bot and the watch one to compile the Typescript code with flag -w(atch), which will watch any changes done to the code and compile upon saving. This is how the package.json file should look like after your changes:
package.json
{
"name":"discord-bot",
"version":"1.0.0",
"description":"",
"main":"index.js",
"scripts":{
"start":"node dist/index.js",
"watch":"tsc -w -p tsconfig.json"
},
"author":"",
"license":"ISC",
"dependencies":{
"@types/node":"^14.14.37",
"discord.js":"^12.5.3",
"dotenv":"^8.2.0",
"inversify":"^5.0.5",
"reflect-metadata":"^0.1.13",
"typescript":"^4.2.3"
}
}
Now that you have set up the scripts, it is time to define the TS compiler for your project.
Defining Typescript compiler
Create tsconfig.json in the root directory and add the following snippet to the file:
"experimentalDecorators": true,
"emitDecoratorMetadata": true,
"resolveJsonModule": true
tsconfig.json
{
"compilerOptions":{
"module":"commonjs",
"moduleResolution":"node",
"target":"es2016",
"lib":[
"es6",
"dom"
],
"sourceMap":true,
"types":[
"node",
"reflect-metadata"
],
"typeRoots":[
"node_modules/@types"
],
"experimentalDecorators":true,
"emitDecoratorMetadata":true,
"resolveJsonModule":true
},
"exclude":[
"node_modules"
]
}
We are adding experimentalDecorators, emitDecoratorMetadata, es6, and reflect-metadata because InversifyJS requires it. Once we have created this, we can create index.ts inside the src folder and run something like console.log(“Hello”) just for testing purposes.
index.ts
console.log(“Hello”)
If everything is correct, run npm run watch which should create src/index.js, and then run npm start which returns the simple message Hello.
The next step is to create a Discord app and connect a Bot to your server.
Creating an application on Discord
Our goal is to communicate and work with the Discord server, but before you do that you need to create a Discord bot that will connect to the server.
To create the bot, first, you need to sign in to Discord and create an application. You can do that by visiting here and clicking on the New Application button at the top right corner.
Clicking the New Application button will prompt you to the next step, where you need to choose the NAME of your application. Whenever you are ready, click Create.
Now, you should see an Application screen with the possibility to enter information about the application - you can skip it for this tutorial.
To create a bot, click Bot and then the Add Bot button. It will tell you that this action is irrevocable and ask you if you want to create a bot. After clicking Yes, do it! You will create a Discord bot. So simple, right?
A page, including the token you need, should open up on your screen right now.
Creating and adding a Discord bot to your server
To work with and later on test your bot, you need a Discord server. You need to either create a new Discord server or use an existing one. Whatever you decide, connecting will look the same.
To add the bot to your server, follow these steps:
- Copy APPLICATION_ID from the General Information tab.
- Change APPLICATION_ID in the following URL to the one you copied
- Open the URL in a browser of your choice to start the authentication process.
- Authorize the process of adding the bot to the server through the steps provided by Discord.
- Once the bot is successfully added, you will get a message in the default text channel.
Now that you have added a bot to the server, it is time to set up the environment in your project.
Saving variables in environment files
To safely provide a token to our application, we are using the dotenv package. You need to get the Bot token from the Discord Application developer dashboard. It is located at Bot > Click to Reveal Token. After you have the token, you need to safely store it in the .env file in the following way:
DISCORD_TOKEN=paste.the.token.here
One more thing we need to create for the future is the .env.example file. It explains how to define the .env file. Inside of the .env.example place:
DISCORD_TOKEN=
Dependency Injection Container
For an easier understanding of dependency injections, you should already have a solid knowledge of the SOLID principle. If you do, you will have no issue understanding what dependency injections are used for.
In our case, we use InversifyJS for dependency injections, and that is why we need to create our DI container in the inversify.config.ts file:
import "reflect-metadata"
import { Container } from "inversify"
import { TYPES } from "./types"
import { Bot } from "./bot"
import { Client } from "discord.js"
let container = new Container()
container.bind < Bot > (TYPES.Bot).to(Bot).inSingletonScope()
container.bind < Client > (TYPES.Client).toConstantValue(new Client())
container.bind < string > (TYPES.Token).toConstantValue(process.env.TOKEN)
export default container
One more thing we need to do for dependency injections is to define types. This will give us the security to not have name collisions with containers. Even though it doesn’t matter that much for us in this tutorial, it is good practice to follow the clean architecture and best practices even when developing a project like this.
types.ts
export const TYPES = {
Bot: Symbol("Bot"),
Client: Symbol("Client"),
Token: Symbol("Token"),
}
Creating a Bot Class
We will create bot.ts inside the src folder and add the following:
import {Client, Message} from "discord.js"
import {inject, injectable} from "inversify"
import {TYPES} from "./types"
import {CommandResponder} from "./services/command-responder"
@injectable()
export class Bot {
private client: Client
private readonly token: string
constructor( @inject(TYPES.Client) client: Client, @inject(TYPES.Token) token: string ) {
this.client = client
this.token = token
}
public listen(): Promise < string > {
this.client.on('message', (message: Message) => {
console.log("We got message, it says: ", message.content)
});
return this.client.login(this.token)
}
}
After this, you can add the bot to index.ts:
require('dotenv').config()
import container from "./inversify.config"
import {TYPES} from "./types"
import {Bot} from "./bot"
let bot = container.get<Bot>(TYPES.Bot)
bot.listen().then(() => {
console.log('Logged in!')
}).catch((error) => {
console.log('Oh no! ', error)
})
With this done, we have created our basic logic and we can start the project and connect the bot with the Discord server.
node src/index.js
Logged in!
We got message, it says: Test
These are your project foundations. The next step is to develop the logic for listening and responding to messages.
Responding to messages
Now, you need to create the logic that listens to certain types of messages and responds to them accordingly. Specifically, you want the bot to listen to two commands: !who and !roll. These two commands will make the bot introduce himself and roll one number between 1 and 100. Then, you need to create two classes - CommandListener and CommandResponder. Do it by injecting CommandResponder into the Bot class and CommandListener into the CommandResponder. But first, we need to create command-listener.ts inside src/services.
import { injectable } from "inversify"
@injectable()
export class CommandListener {
private regexp = ['!who', '!roll']
public isWhoCommand(string: string): boolean {
return string.search(this.regexp[0]) >= 0
}
public isRollCommand(string: string): boolean {
return string.search(this.regexp[1]) >= 0
}
}
You can inject that to command-responder.ts, which you can create inside src/services.
import {Message} from "discord.js"
import {CommandListener} from "./command-listener"
import {inject, injectable} from "inversify"
import {TYPES} from "../types"
@injectable()
export class CommandResponder {
private commandListener: CommandListener
constructor(
@inject(TYPES.CommandListener) commandListener: CommandListener ) {
this.commandListener = commandListener
} handle(message: Message): Promise<Message | Message[]> {
if (this.commandListener.isWhoCommand(message.content)) {
return message.reply('I am bot that is made to demonstrate connection between NodeJs and Discord.')
}
if (this.CommandListener.isRollCommand(message.content)) {
const rolled = Math.floor(Math.random() * 100) + 1 return message.reply(`You have rolled ${rolled}`)
}
return Promise.reject()
}
}
And then, the Bot class needs to be changed to use the CommandResponder class:
import {Client, Message} from "discord.js"
import {inject, injectable} from "inversify"
import {TYPES} from "./types"
import {CommandResponder} from "./services/command-responder"
@injectable() export class Bot {
private client: Client
private readonly token: string
private commandResponder: CommandResponder constructor(
@inject(TYPES.Client) client: Client,
@inject(TYPES.Token) token: string,
@inject(TYPES.CommandResponder) commandResponder: CommandResponder) {
this.client = client
this.token = token
this.commandResponder = commandResponder
}
public listen(): Promise<string> {
this.client.on('message', (message: Message) => {
if (message.author.bot) { console.log('Ignoring my own or other bot messages.')
return
}
this.commandResponder.handle(message).then(() => {
console.log("A response sent!")
}).catch(() => {
console.log("There was an error with sending a response.")
})
})
return this.client.login(this.token)
}
}
One last thing to do is create new DI containers and types for CommandResponder and CommandListener.
Remember how you bound containers before for token, Bot, and Client classes? Now you need to do the same for CommandResponder and CommandListener.
inversify.config.ts
container.bind<CommandResponder>(TYPES.CommandResponder).to(CommandResponder).inSingletonScope()
container.bind<CommandListener>(TYPES.CommandListener).to(CommandListener).inSingletonScope()
You also need to add these two types, so that you don’t create a bug by the last addition to the inversify.config.ts. An error will occur because you used CommandResponder and CommandListener types which are not declared in types.ts.
types.ts
CommandResponder: Symbol("CommandResponder"),
CommandListener: Symbol("CommandListener")
And voila! Your Discord bot is ready to listen to your two new commands and respond to them accordingly. The only thing left to do is to restart the app and test the bot.
After testing our commands, this is the response you get:
This is the response you should expect - it shows everything works as developed.
What’s next?
First of all, congratulations! You just made it through the whole article and (hopefully) learned how to do a Discord bot. But you might say - OK, that's cool and all. But what can I do with it? Well, you can play around and create more complex commands, connecting Discord to your external database or integrating Discord with some type of API (such as OMDB API or any other of the free ones). However, since I covered the basics in this article, I suggest you research larger communities and learn about all the commands and the complexity of Discord bots.
If you have any questions or ideas regarding this blog post, please feel free to reach out. And most importantly, have fun coding! :)
Posted on February 7, 2024
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.