My Discord Bot Stack


After a few years of working on Discord bots, I’ve decided to write a post about my preferred stack. I’ve been using a lot of different technologies and I’m going explain why I chose them.

The Runtime

Node.js is the most used runtime for Discord bots. Thanks to the wide popularity of JavaScript on the web, many developers find it easy to write server-side applications in JavaScript because of the huge community support behind it. Almost every functionality you need is available as a package on npm, and most packages are well maintained and documented. Running code on Node.js is straightforward - write your code in a file and run it with node <filename>, no compilation needed. Node.js is also extremely quick and can handle a lot of requests at once, making it a great choice for a Discord bot.

discord.js

discord.js is my library of choice for interacting with the Discord API. It’s by far the most used and most maintained wrapper around the Discord API. Discord.js is constantly up to date with the latest Discord API changes, meaning you can use the latest features without having to wait. It also has a fantastic documentation and guide, making it easy to get going with.

Here is a simple example of a command written using discord.js that replies to /ping with ‘Pong!‘:


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

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

Sapphire.js

Okay so writing a Ping-Pong command is fairly easy, but what if we want to add more commands and more complex functionality? By complex I mean things like preconditions, permissions, and argument parsing. This is where Sapphire.js comes in. Sapphire is a framework built on top of discord.js that enables us to write more complex bots with ease. It has built it features like preconditions, argument parsing, and command cooldowns.

Here is the same Ping-Pong command written using Sapphire:

import { ApplyOptions } from "@sapphire/decorators";
import {
  ApplicationCommandRegistry,
  Command,
  CommandOptions,
} from "@sapphire/framework";
import type { CommandInteraction } from "discord.js";

@ApplyOptions<CommandOptions>({
  name: "ping",
  description: "Replies with Pong!",
  preconditions: ["isCommandDisabled"],
})
export class PingCommand extends Command {
  public override async chatInputRun(interaction: CommandInteraction) {
    const ping = interaction.createdTimestamp - Date.now();
    const apiPing = Math.round(interaction.client.ws.ping);
    return await interaction.reply(
      `Pong! - Bot Latency: ${ping}ms - API Latency: ${apiPing}ms - Round Trip: ${
        ping + apiPing
      }ms`
    );
  }

  public override registerApplicationCommands(
    registry: ApplicationCommandRegistry
  ): void {
    registry.registerChatInputCommand({
      name: this.name,
      description: this.description,
    });
  }
}

Notice the @ApplyOptions decorator? This is a decorator provided by Sapphire that allows us to define the command’s name, description, and preconditions. In this example, we’re using the isCommandDisabled precondition, which is a custom precondition that doesn’t allow the command to be used if it’s disabled.

tRPC

tRPC is the part of the stack that I’m most excited about. tRPC is a library that enables us to write end-to-end fully typesafe APIs with minimal boilerplate and no schemas and code generation. That means a developer experience from heaven - no more runtime errors due to type mismatches. tRPC provides useQuery and useMutation hooks that are a thin wrapper around the hooks provided by React Query, but with the added benefit of type safety.


For example, if we pass a wrong type to useQuery, we’ll get a type error:

const { data, isLoading } = trpc.useQuery([
  "guild.get-guild",
  {
    id: query,
  },
]);

In this case the ‘get-guild’ endpoint expects a string as the id. If we’ll pass a number instead, we’ll get a type error on our IDE before we even run the code.

Prisma

Prisma is an ORM that enables us to write type-safe database queries. The Prisma client can be used to safely query both SQL and NoSQL databases, such as PostgreSQL, MySQL, MongoDB, and SQLite. Prisma does all the heavy lifting for us, so we don’t have to worry about writing SQL queries or dealing with database migrations.


With Prisma, we can write models that represent our database tables:

generator client {
  provider = "prisma-client-js"
}

datasource db {
  provider          = "postgresql"
  url               = env("DATABASE_URL")
}

model Guild {
  id                    String        @id
  name                  String
  ownerId               String
  owner                 User          @relation(fields: [ownerId], references: [discordId])
}

And then use them to write type-safe queries:

const guild = await prisma.guild.findUnique({
  where: {
    id: interaction.guildId,
  },
});

Next.js

Next.js is a React framework that let’s us build web applications. React is merely a library for building UIs, it provides us with helpful functions to build UI but it doesn’t provide us with a way to build a full application. Building a React app from scratch requires some effort, setting up tools like Webpack, Babel, and TypeScript. Next.js takes care of all that for us, so that we can focus on building our app. Next gives us features like Routing, Server-Side Rendering, Static Site Generation, and Data Fetching, all out of the box. Without it, we’d have to install and setup a lot of different packages to get the same functionality. Next also integrates like a charm with tRPC, allowing us that sweet end-to-end type safety. All these features make Next.js the perfect choice for building a Discord bot dashboard.

Summary

In this post I’ve described the pieces of the stack I use to build advanced Discord bots. I didn’t go into connecting the pieces together, because I wanted to give a high-level overview of the stack. I’ll be writing a follow-up post that will go into how I connected the pieces together in my popular music bot, and the challenges I faced along the way.