Back to the build log

Building a Discord bot for 10,000 users

How it started

In 2021, during the COVID-19 lockdowns, I suddenly had a lot more free time and wanted to spend it on something productive. I met a friend who was older than me and already had coding experience and he helped me while I learnt. My first idea was just to make my own Discord bot and experiment with it.

Scaling it up

The more I learnt, the more I wanted to build something people would actually use. I noticed that the bots already on Discord didn’t do everything servers wanted, so I started adding the features members and staff were asking for. The bot spread to more servers, and one of the biggest belonged to a YouTuber who now has over 400,000 subscribers.

I was learning the language and the Discord API while people were already using the bot, so commands broke and I had to work out why and fix them while it was live. Somewhere in that process the lockdown experiment became something I really cared about somehow.

Where it led

This was the first thing I built that other people relied on. It taught me programming and problem solving, and showed me how much a project improves when you listen to the people using it. It’s how I found out I enjoy building things from scratch, and it’s why I stayed with tech and kept starting new projects.

What the bot does

Moderation

ban and kick check that whoever runs the command has the BAN_MEMBERS or KICK_MEMBERS permission. They refuse to act on administrators and check that the bot’s own role is high enough to act (bannable / kickable). Before removing someone, the bot DMs them the reason, and banned users also get an appeal link.

sm sets a channel’s slowmode. It rejects anything that isn’t a number and keeps the value within Discord’s limit of 1 to 21,600 seconds. purge bulk-deletes messages, then posts a confirmation that deletes itself after six seconds.

Verification and roles

New members get a welcome embed linking the rules, announcements and general channels, plus a DM telling them how to verify. When they run verify, the bot gives them the verified role, deletes their command and logs an embed to a staff channel with their account creation date, join date, status and user ID. Staff could use that to spot new or suspicious accounts.

Members can also give themselves colour roles and notification roles (announcements, giveaways, new content, chat revival) with one command, without needing a moderator.

Modmail

report <user> <reason> deletes the public message, confirms to the reporter by DM, and sends a report embed with the reported member’s details to a private staff channel. question sends a member’s question to staff, who reply privately with answer or action, and the bot delivers the reply by DM. Members could contact the team without pinging moderators in public.

Key decisions and why

I built my own bot because servers needed moderation, verification, roles and modmail in one place, set up the way they wanted. Doing it myself meant I could add what people asked for straight away.

Reports, verification logs and the help menu use Discord’s rich embeds, so staff can read the important fields at a glance.

Every moderation command checks permissions and role hierarchy before it acts. Ordinary members can’t use moderation commands, and moderators can’t act on admins.

The bot deletes report, question and verification commands as soon as they’re sent, which keeps them private and stops channels filling up with bot commands.

Hosting

The bot ran 24/7 as a worker dyno on Heroku’s free plan, started from a Procfile.

Looking back at the code

Reading the code again now, I can see a few weaknesses. Everything lives in a single index.js with a long switch statement, which made every new feature harder to add safely. Channel and role IDs are written straight into the code, so setting it up for a different server meant editing the code itself.

The help menu also lists a couple of commands under names that don’t match the real ones, because I changed commands without updating it. Error handling is uneven too. Some DMs use .catch() so a member with closed DMs doesn’t break a command, but others don’t.

What I’d do differently

  • Split commands into separate modules loaded by a command handler.
  • Move server-specific IDs into a config file so the same code could run in any server.
  • Switch to slash commands, which discord.js v13 introduced, so Discord validates arguments before they reach the bot.
  • Keep the bot token in an environment variable from the start and never commit it.
  • Handle errors on every API call that can fail.

Skills I developed

  • Programming: JavaScript, Node.js, asynchronous code with promises and async/await, event-driven programming, error handling, debugging a live system
  • Discord API: Gateway events, the permissions system and role hierarchy, embeds, DMs, bulk message deletion, channel rate limits
  • Product: gathering feature requests and feedback from real users, deciding what to build next, maintaining something people rely on

Thank you

Thank you to the friend who mentored me when I was starting out, and to the server owners and members who used the bot and told me what to build next.