Nextjs
Discord
TypeScript
React
Prisma
Vercel

hbd.mjanglin.com

This is the updated version of the hbd bot. To view the original project built with discord.js and nodejs runtime, visit this page.

This page is a detailed explanation of the hbd bot, a Discord birthday bot built with Next.js, Prisma, and the Discord API.

  • This walkthrough will cover the structure, commands, and Prisma integration of the bot.

Synopsis

The primary purpose of this bot is to provide fun and interactive commands that involve birthdays, holidays, horoscopes, and more.


Template

The initial template for a bot with edge functionality was created by @jzxhuang:

I built off his template to create another template that implements OAuth2 which you can use to create your own variation of this bot:

git clone https://github.com/clxrityy/nextjs-discord-bot-with-oauth.git 

Structure

/
├── prisma/ <-- Prisma schemas
├── public/ <-- Public assets
├── scripts/ <-- External scripts
├── src/ <-- Source code
├── ...
Source Code
src/
├── app/ <-- Routes within the website
├── components/ <-- Components within website
├── data/ <-- Bot data
│   ├── commands/ <-- Bot commands
│   ├── util/ <-- Bot utilities
├── handlers/command/ <-- Command handlers
├── lib/ <-- External libraries
├── types/ <-- Types throughout the application
├── utils/ <-- Utility functions
├── config.ts <-- The configuration values
├── env.mjs <-- Environment variables
├── ...
  • For a more in-depth understanding on Nextjs routes & React components: Learn Nextjs.
  • An elaborated explanation of all the directories and files within our source code can be found in the Github repository here.

Commands

Defining & Registering

Commands are defined as an ApplicationCommand type.

Application command type: (reference)
export type ApplicationCommandOption = {
    type: number;
    name: string;
    // ...
}
 
export type ApplicationCommand = {
    name: string;
    description: string;
    options?: ApplicationCommandOption[];
    permissions?: number[];
}
Command definition example
import { ApplicationCommand } from "@/types/interactions";
 
const PING_COMMAND: ApplicationCommand = {
    name: "ping",
    description: "Replies with Pong!"
} as const;
 
export default PING_COMMAND;

Every command is exported in the primary index.ts file:

/**
 * @see https://discord.com/developers/docs/interactions/application-commands#registering-a-command
 */
import PING_COMMAND from "./misc/ping";
 
export const commands = {
    ping: PING_COMMAND,
}

To register commands, run the pnpm register-commands script.

import { commands } from "@/data/commands";
import { env } from "./env.mjs";
 /**
 * @see https://discord.com/developers/docs/interactions/application-commands#bulk-overwrite-global-application-commands
 */
async function main() {
    const reponse = await fetch(`http://discord.com/api/v10/applications/${env.CLIENT_ID}/commands`, 
    {
        headers: {
            "Content-Type": "application/json",
            "Authorization": `Bot ${env.BOT_TOKEN}`
        },
        method: "PUT",
        body: JSON.stringify(Object.values(commands))
    });
}

Interaction API

Within the interactions endpoint, data from the interaction is parsed into an InteractionData interface:

export interface InteractionData {
    id: string;
    name: string;
    options?: InteractionSubcommand<InteractionOption>[] | InteractionOption[] | InteractionSubcommandGroup<InteractionSubcommand<InteractionOption>>[];
}

This includes the value(s) of the option(s) and subcommands.

Interaction Response

This is where the edge runtime comes into play.

// api/interactions/route.ts
 
/**
 * @see https://nextjs.org/docs/app/building-your-application/rendering/edge-and-nodejs-runtimes
*/
export const runtime = "edge";

The interaction is received and verified.

export async function POST(req: Reqest) {
    /**
    * @see https://github.com/clxrityy/mjanglin.com/blob/hbd/src/utils/verify.ts
    */
    const verifyResult = await verifyInteractionRequest(req, env.PUBLIC_KEY);
 
    const { interaction } = verifyResult;
}

The interaction data is parsed into an InteractionData interface which contains all the parameters needed to respond to the interaction.

import {
    InteractionResponseType,
    InteractionType,
} from "discord-api-types/v10";
import { InteractionData } from "@/types/interactions";
 
if (interaction.type === InteractionType.ApplicationCommand) {
    const { name } = interaction.data;
    const interactionData: InteractionData = JSON.parse(JSON.stringify(interaction.data));
}

A response is sent back to Discord based on the command name in the form of an interaction response object.

import { InteractionSubcommand, InteractionOption } from "@/types/interactions";
// ...
 
switch (name) {
    case commands.ping.name:
        return {
            type: InteractionResponseType.ChannelMessageWithSource,
            data: {
                content: "Pong!"
            }
        }
}

Prisma

Prisma is used to manage data stored with Vercel Postgres.

pnpm add @prisma/client @prisma/extension-accelerate
pnpm add --save-dev prisma

To initialize the Prisma client, run the npx prisma generate script. You can now use the Prisma client to interact with your database.

// src/lib/db.ts
import { PrismaClient } from "@prisma/client";
import { withAccelerate } from "@prisma/extension-accelerate";
 
function makePrisma() {
    return new PrismaClient({
        datasources: {
            db: {
                url: process.env.ACCELERATE_URL
            }
        }
    }).$extends(withAccelerate());
}

See this guide for more information on setting up Prisma with Vercel Postgres.

Schemas & Models

Prisma schemas are defined in the prisma/ directory.

The first necessary schema is the User model which will ease the process of storing user data through OAuth2.

model User {
    id           String     @id @default(uuid())
    userId       String     @unique
    accessToken  String     @unique
    refreshToken String     @unique
    // More added later
}

After defining the schema, run the npx prisma db push script to create the table in your database, then npx prisma generate to re-generate the Prisma client with the new schema.

Now, you can interact with the User model in your application.

import { db } from "@/lib/db";
 
export async function getUser(userId: string) {
    return await db.user.findFirst({
        where: {
            userId: userId
        }
    });
}

Conclusion

The bot is built with Next.js, Prisma, and the Discord API. It is designed to be easily customizable and extendable with new commands and features.

This bot is still a work in progress. The goal is to create a dynamic Discord bot that can be easily deployed and managed with Vercel.

If you have any ideas for improvements, please feel free to open a pull request.

If you come across any bugs or issues, please open an issue.

  • Clone the template repository here

Thank you for reading! ❤️