I've been dying to try out Discord's new slash commands for a few months now, but unfortunately the framework I use for my bots (discord.js) hasn't shipped official support for them yet. I finally got impatient enough to decide to write a slash command bot using the new HTTP API (instead of the persistent Gateway connection), which is a lot easier to use without a library wrapper.
As it turns out, there aren't a lot of tutorials on how to do this yet. I thought it would be useful to publish how I did it - using the HTTP API is a great way to create simple bots! My project idea was to create a bot that imitates the old /slap <user>
IRC command.
If you want to see what we'll be building, you can add my hosted version to your server here.
Creating a Discord Bot
First things first - we need to create an official Discord Application for our slash command bot. Sign into the Discord Developer Portal and click "New Application" in the top right corner. Give it a name, and click create. Next up, navigate to "Bot" in the sidebar and click the "Add Bot" button. Verify with "Yes, do it!" on the warning prompt that comes up. With that, we'll have created our application! There are a few values we'll want to grab out of this portal.
- Token: On the "Bot" sidebar tab there's a Token value you can copy - we'll be using this to authenticate our server. This value should never be published in public!
- Application ID: On the "General Information" sidebar tab there's a field called Application ID. We'll need this to create an invite link.
- Public Key: On the "General Information" sidebar tab there's a field called Public Key. We'll be using this to authenticate interaction requests.
We'll be copying these three values into a .env
file in our project - more on that below. Also take note of the "Interactions Endpoint URL" field on the "General Information" tab. We'll be using that to link to our server.
Keep this tab open and the values handy. Now let's dive into programming our new bot!
Coding the Bot
Let's start by creating a new folder for our project and initializing it with npm.
terminal
$mkdir trout && cd trout$npm init
For this particular project, let's use esm by default. All of the supported Node.js versions now fully support it (you can check what version you're on by running node -v
in your terminal - it should be 10+) so let's make it a habit to use esm!
We can also add a start
script that loads our bot's tokens from a .env
file and boots the server.
package.jsonjson
1{2 "name": "discord-trout",3 "version": "1.0.0",4 "description": "Sometimes you gotta slap a person with a large trout",5 "main": "server.js",+ "type": "module",7 "scripts": {+ "start": "node -r dotenv/config server.js"- "test": "echo \"Error: no test specified\" && exit 1"10 },11 ...12}
Let's get things kicked off by installing some dependencies for our project.
dotenv
is used to load our Bot's secret Tokens from a.env
file. This file should not be committed into your source code. If you are using git, add.env
to your.gitignore
file.fastify
is the server framework we'll be using. If you've used express before, it's ergonomically similar - there are performance benefits to using it though, and it comes with out of the box async/await support.discord-interactions
provides some utilities that we'll need when interacting with the Discord API.
terminal
$
Now we can create a .env
file and copy over our bot information from Discord's Applications page. Replace the values with the ones from the bot you created earlier.
.envtext
123
With that done, we can move onto the code! We'll start by creating a minimal fastify server. We'll copy most of the code from the fastify getting started guide, and add a GET request for the root URL.
server.jsjavascript
1234567891011121314151617
If you start the server with npm start
you should be able to navigate to http://localhost:3000 and see a message in your terminal that fastify has received and responded to a request!
Now it's time for the fun part - let's start writing the bot logic. The slash command documentation starts off by telling us that we need to respond to a PING
interaction type that it will send via a POST request. Let's modify our server code to define a new POST route on the root url, and then have it look at the request object to determine if it's a PING
interaction or not. If it is, we should respond with the appropriate PONG
interaction. The discord-interactions
library we installed earlier has a couple of helpers we can use to make our code clearer!
server.jsjavascript
1+34567891011++14++++++++++2526272829303132
If we start our server and try to connect our application to Discord at this point, we'll get a failure message. It turns out that our application can't accept every request that it is sent - we need to do some verification work to ensure the request is valid.
There's a helper method in discord-interactions
to verify requests, but it requires the raw request body to work. Fastify doesn't have built-in support for that, but luckily there is a community plugin we can set up to get things working. We'll want the plugin to run and transform the request before any handlers run, so we'll register it in fastify with a runFirst
option set.
terminal
$
server.jsjavascript
1+345678+++121314151617
Great! Now we have a request.rawBody
value that discord-interactions
needs. Next we need to set up a fastify hook that checks all incoming requests - we'll want this check to run before any route handler logic, so we'll register it as a preHandler hook. The discord-interactions library explains how to use it to verify a request, so we'll largely copy over what they have written while making some small tweaks to fit it to fastify instead of express.
server.jsjavascript
1+345+7891011121314151617181920++++++++++++++++++39404142434445464748495051525354555657585960
Now if we start the server and try to connect it with the Discord applications page, we should get a success message!
It's finally time to begin working on our commands. We'll create two of them - one that implements the famous /slap
command from IRC, and another that sends a bot invitation link to users who also want to add the bot to their own servers.
For our Slap command, we'll need the person interacting with the bot to input a user to target. The Discord API docs say that a user mention is type 6 - we'll mark that option as required, since we need someone to slap. The invite command doesn't require an input, so we'll just define it with a name and description.
Since we'll need to register the commands with Discord when we're done defining them, we'll put the definitions in a new commands.js
file.
commands.jsjavascript
1234567891011121314151617
Then we can import them and add some helper definitions back in our server code:
server.jsjavascript
1234567+9+11121314151617
Next, let's write the code that handles these interactions! We'll go back into our POST route and add checks for someone using our slash commands (the Discord API refers to these events as an Application Command Interaction). The docs have an example schema for an interaction - we can use that to write our function definition.
To start with, we'll want different responses depending on which slash command was used. We can use a switch
statement that looks at the command name, and then write out our different responses accordingly.
For our slap message, we want it to display as {user} slaps {target} around a bit with a large trout. Discord messages use simplified Markdown formatting, with special sequences you can use to mention different things - to get this display, we'll format our message like this:
javascript
`*<@${userId}> slaps <@${targetId}> around a bit with a large trout*`;
For our invitation link, let's make the response message ephemeral1 so we don't spam servers. The Discord API docs say that if you want to make a response ephemeral, all you need to do is pass flags: 64
into the response!
Let's see what the completed interaction handler looks like for our server.
server.jsjavascript
12345678910++++++++++++++++++++++++++37383940414243
Now that we have our handler finished, it's time to register our commands with Discord! We'll create a special register
script here that we'll call manually. We'll import our command definitions from server.js
and then make a PUT request to the commands API with them.
register.jsjavascript
12345678910111213141516171819202122
We can add an entry into our package.json
file for ease of use:
package.jsonjson
1{2 // ...3 "scripts": {+ "register": "node -r dotenv/config register.js",5 "start": "node -r dotenv/config index.js"6 },7 // ...8}
Running it should successfully register the commands:
terminal
$npm run register
If you boot up your server, you should see the slash commands in your guild! Go ahead and try them out - everything should work!
Next Steps
Now that you have a finished slash command bot, there are a few places you can go from here. For starters, add some more commands! There's a lot you can do with slash commands. Have some fun trying things out! You can also look into hosting this bot on DigitalOcean, AWS, or something else so that you don't need to worry about running it locally. Personally, I took my finished bot and turned it into a serverless function hosted on Vercel. I'll be writing a (shorter) post on how I did that sometime this week, but if you're curious the current source code can be found at IanMitchell/discord-trout.
There's also a lot of room for code improvement - I have a few ideas of things I want to try to make more of a slash command framework, but there is a lot to explore. Try making writing some reusable functions to make your own personal framework!
If you have any questions or suggestions, shoot me a tweet or email. I'm happy to write more on this subject and do more tutorials on Discord bots if anyone is interested. If there's something you want to do but aren't sure how, let me know and I'm happy to try and help out!
- An ephemeral message is a message that's only sent to a single user - it won't be visible for anyone else in the server.↩