Gauntlet DesignsArticlesAI Solutions

Building a Serverless Discord Bot With NextJS

Discord bots are typically powered by a application running in a server. There is a better way. We can utilize the power of NextJS and Vercel to build a serverless (to us) discord bot.

Setting Up NextJS

First, we must start our code base using the nextjs CLI.

npx create-next-app my-discord-bot --typescript --tailwind

Take the defaults for all answers.

Setting Up Github and Vercel

Alright, time to get our continuous integration, continuous deployment (CI/CD) systems set up. That will allow us to deploy to our "production" application by simply pushing our code to Github.

Let's create a new repository in Github. In this case we'll call it my-serverless-bot

Creating a new github repository for gauntletdesigns/my-serverless-bot

Once the repository is made, copy the section titled "…or push an existing repository from the command line". Paste this into terminal within your code base to link the code to the repository.

Next, you'll need to create an account with Vercel. After connecting this account to Github, you will be able to import the repository.

Vercel git repository connection

On the following page, simply select deploy. This is all we need to do here for now. After a few minutes you will be able to access the deployed page via the project page on vercel.

Setting Up Discord Application

Discord provides our applications with webhooks for interactions. When a user uses a slash command discord will call an endpoint within our application. In order to use that we must verify the requests are from Discord.

Verifying Discord Request

Install a node package called discord-interactions.

npm i discord-interactions

Create a folder within the app folder called interactions. Then create a route.ts within that folder. This is the default file that nextjs uses to provide an API endpoint on /interactions. Discord makes a post request to that route, so we need to setup a post function.

import {NextRequest} from "next/server";

export async function POST(req: NextRequest){}

Setting Up Env Variables

Prior to doing any work with the requests we must verify the request. This allows us to trust that the request is from Discord, and not anyone else. To do this, we must setup a .env.local at the root of our project, and add the Discord Public Key from our discord application page. We'll also store our bot's id from this page.

NEXT_PRIVATE_DISCORD_ID=<application id>
NEXT_PRIVATE_DISCORD_PUBLIC_KEY=<public key>

Now in the Vercel project paste the same env variables into project settings.

Handling Verifications

When a verification fails we need to throw an error to handle the issue. Lets make a utils folder within app, and a exceptions.ts within that.

export class ValidationException extends Error {
  public errorType: string;
  public status: number;
  constructor(public message: string, public statusNumber: number){
    super(message);
    this.errorType = 'Validation'
    this.status = statusNumber
  }
}

Back in our route.ts file, we need to verify the request.

import {NextRequest, NextResponse} from "next/server";
import {headers} from "next/headers"
import {verifyKey} from "discord-interactions";
import {ValidationException} from "@/app/utils/exceptions";

const verifySignature = async (body: any) => {
  const signature = headers()?.get('X-Signature-Ed25519') as string;
  const timestamp = headers()?.get('X-Signature-Timestamp') as string;
  const isValidRequest = verifyKey(JSON.stringify(body), signature, timestamp, process.env.NEXT_PRIVATE_DISCORD_PUBLIC_KEY!)
  if (!isValidRequest){
    throw new ValidationException("Invalid request signature", 401);
  }
}

export async function POST(req: NextRequest){
  const body = await req.json();
  try {
    await verifySignature(body);
    // handle interaction responses here
  } catch (e: any) {
    console.error("Error found", e?.message);
    if (e.errorType === 'Validation'){
      return NextResponse.json({
        message: e.message
      }, {
        status: e.status
      })
    }
  }

}

Now that we have the error case handled, let's setup the happy path.

...
const handleResponse = async (body: any) => {
  const {data} = body;
  switch(data.name){
    case 'PING':
      return NextResponse.json({type: 1})
  }
}

export async function POST(req: NextRequest){
  const body = await req.json();
  try {
    await verifySignature(body);
    return await handleResponse(body);
...

Commit and push. Once the app is deployed with vercel we'll move to the next step.

Sending Our First Message To The App

Back in our discord application identify the "Interactions Endpoint URL". Grab your endpoint from Vercel, and paste it into this field with /interactions. If this fails to save your verification is not correct. Check the previous sections to ensure your verification setup is right.

Now for the exciting part, time to work on our first command. We will setup a simple Hello command.

In our interactions folder make a hello.ts. Hello is a function that responds with the discord interaction response defined here

import {NextResponse} from "next/server";

export const Hello = async () => {
  return NextResponse.json({
    type: 4,
    data: {
      content: 'Hello World'
    }
  })
}

In our interactions/route.ts file. Add the hello function to the function handler.

import {Hello} from "@/app/interactions/hello";
...
const handleResponse = async (body: any) => {
  const {data} = body;
  console.log(data);
  switch(data?.name){
    case 'hello':
      return await Hello();
  }
  return NextResponse.json({type: 1})
}
...

Registering Command With Discord

Last thing to setup here is a command to register the interactions with discord. Back in the discord application page, grab your bot token, and store it in your .env.local file.

Screenshot of discord bot token setup
// .env.local
...
NEXT_PRIVATE_DISCORD_BOT_TOKEN=<token from discord>
...

Create a new folder called scripts inside of the app folder. Within that make a folder called commandSetup then a route.ts within that.

import axios from 'axios';
import {NextResponse} from "next/server";

// setup auth for discord
const discordHeaders = {
    Authorization: `Bot ${process.env.NEXT_PRIVATE_DISCORD_BOT_TOKEN}`
}

export async function GET(){
    // save the url for adding commands
    const url = `https://discord.com/api/v10/applications/${process.env.NEXT_PRIVATE_DISCORD_ID}/commands`

    const data = {
        name: "hello",
        type: 1,
        description: 'Say Hello'
    }

    await axios({
        method: 'POST',
        url,
        data,
        headers: discordHeaders
    })

    return NextResponse.json({})
}


Add Axios

npm i axios --save

To Fire the command visit your local website at /scripts/commandSetup.

Adding Our Bot To A Server

Back in discord, go to the OAuth2 -> URL Generator. Select Bot under scopes, and Admin under bot permissions.

Copy the URL and paste it in to your browser, select the server to add your bot to.

In your server you should now be able to type /hello and see the command.

Responding To A Discord Message

Last step, make sure the bot token environment variable is set in vercel settings. Commit and push your changes.

Congrats you now have a working discord bot. Type /hello in chat and your bot should respond.

Gauntlet Designs
Contact us
contact@gauntletdesigns.com
Gauntlet Designs