Discord Bot Architect Patterns Discord.js v14 Foundation
Modern Discord bot setup with Discord.js v14 and slash commands
When to use: ['Building Discord bots with JavaScript/TypeScript', 'Need full gateway connection with events', 'Building bots with complex interactions']
```javascript // src/index.js const { Client, Collection, GatewayIntentBits, Events } = require('discord.js'); const fs = require('node:fs'); const path = require('node:path'); require('dotenv').config();
// Create client with minimal required intents const client = new Client({ intents: [ GatewayIntentBits.Guilds, // Add only what you need: // GatewayIntentBits.GuildMessages, // GatewayIntentBits.MessageContent, // PRIVILEGED - avoid if possible ] });
// Load commands client.commands = new Collection(); const commandsPath = path.join(__dirname, 'commands'); const commandFiles = fs.readdirSync(commandsPath).filter(f => f.endsWith('.js'));
for (const file of commandFiles) { const filePath = path.join(commandsPath, file); const command = require(filePath); if ('data' in command && 'execute' in command) { client.commands.set(command.data.name, command); } }
// Load events const eventsPath = path.join(__dirname, 'events'); const eventFiles = fs.readdirSync(eventsPath).filter(f => f.endsWith('.js'));
for (const file of eventFiles) { const filePath = path.join(eventsPath, file); const event = require(filePath); if (event.once) { client.once(event.name, (...args) => event.execute(...args)); } else { client.on(event.name, (...args) => event.execute(...args)); } }
client.login(process.env.DISCORD_TOKEN);
// src/commands/ping.js const { SlashCommandBuilder } = require('discord.js');
module.exports = { data: new SlashCommandBuilder() .setName('ping') .setDescription('Replies with Pong!'),
async execute(interaction) { const sent = await interaction.reply({ content: 'Pinging...', fetchReply: true });
const latency = sent.createdTimestamp - interaction.createdTimestamp;
await interaction.editReply(`Pong! Latency: ${latency}ms`);
} };
// src/events/interactionCreate.js const { Events } = require('discord.js');
module.exports = { name: Event
Pycord Bot Foundation
Discord bot with Pycord (Python) and application commands
When to use: ['Building Discord bots with Python', 'Prefer async/await patterns', 'Need good slash command support']
```python
main.py
import os import discord from discord.ext import commands from dotenv import load_dotenv
load_dotenv()
Configure intents - only enable what you need
intents = discord.Intents.default()
intents.message_content = True # PRIVILEGED - avoid if possible
intents.members = True # PRIVILEGED
bot = commands.Bot( command_prefix="!", # Legacy, prefer slash commands intents=intents )
@bot.event async def on_ready(): print(f"Logged in as {bot.user}") # Sync commands (do this carefully - see sharp edges) # await bot.sync_commands()
Slash command
@bot.slash_command(name="ping", description="Check bot latency") async def ping(ctx: discord.ApplicationContext): latency = round(bot.latency * 1000) await ctx.respond(f"Pong! Latency: {latency}ms")
Slash command with options
@bot.slash_command(name="greet", description="Greet a user") async def greet( ctx: discord.ApplicationContext, user: discord.Option(discord.Member, "User to greet"), message: discord.Option(str, "Custom message", required=False) ): msg = message or "Hello!" await ctx.respond(f"{user.mention}, {msg}")
Load cogs
for filename in os.listdir("./cogs"): if filename.endswith(".py"): bot.load_extension(f"cogs.{filename[:-3]}")
bot.run(os.environ["DISCORD_TOKEN"])
cogs/general.py
import discord from discord.ext import commands
class General(commands.Cog): def init(self, bot): self.bot = bot
@commands.slash_command(name="info", description="Bot information")
async def info(self, ctx: discord.ApplicationContext):
embed = discord.Embed(
title="Bot Info",
description="A helpful Discord bot",
color=discord.Color.blue()
)
embed.add_field(name="Servers", value=len(self.bot.guilds))
embed.add_field(name="Latency", value=f"{round(self.bot.latency * 1000)}ms")
await ctx.respond(embed=embed)
@commands.Cog.
Interactive Components Pattern
Using buttons, select menus, and modals for rich UX
When to use: ['Need interactive user interfaces', 'Collecting user input beyond slash command options', 'Building menus, confirmations, or forms']
```javascript // Discord.js - Buttons and Select Menus const { SlashCommandBuilder, ActionRowBuilder, ButtonBuilder, ButtonStyle, StringSelectMenuBuilder, ModalBuilder, TextInputBuilder, TextInputStyle } = require('discord.js');
module.exports = { data: new SlashCommandBuilder() .setName('menu') .setDescription('Shows an interactive menu'),
async execute(interaction) { // Button row const buttonRow = new ActionRowBuilder() .addComponents( new ButtonBuilder() .setCustomId('confirm') .setLabel('Confirm') .setStyle(ButtonStyle.Primary), new ButtonBuilder() .setCustomId('cancel') .setLabel('Cancel') .setStyle(ButtonStyle.Danger), new ButtonBuilder() .setLabel('Documentation') .setURL('https://discord.js.org') .setStyle(ButtonStyle.Link) // Link buttons don't emit events );
// Select menu row (one per row, takes all 5 slots)
const selectRow = new ActionRowBuilder()
.addComponents(
new StringSelectMenuBuilder()
.setCustomId('select-role')
.setPlaceholder('Select a role')
.setMinValues(1)
.setMaxValues(3)
.addOptions([
{ label: 'Developer', value: 'dev', emoji: '💻' },
{ label: 'Designer', value: 'design', emoji: '🎨' },
{ label: 'Community', value: 'community', emoji: '🎉' }
])
);
await interaction.reply({
content: 'Choose an option:',
components: [buttonRow, selectRow]
});
// Collect responses
const collector = interaction.channel.createMessageComponentCollector({
filter: i => i.user.id === interaction.user.id,
time: 60_000 // 60 seconds timeout
});
collector.on('collect', async i => {
if (i.customId === 'confirm') {
await i.update({ content: 'Confirmed!', components: [] });
collector.stop();
} else if (i.custo
Anti-Patterns ❌ Message Content for Commands
Why bad: Message Content Intent is privileged and deprecated for bot commands. Slash commands are the intended approach.
❌ Syncing Commands on Every Start
Why bad: Command registration is rate limited. Global commands take up to 1 hour to propagate. Syncing on every start wastes API calls and can hit limits.
❌ Blocking the Event Loop
Why bad: Discord gateway requires regular heartbeats. Blocking operations cause missed heartbeats and disconnections.
⚠️ Sharp Edges Issue Severity Solution Issue critical ## Acknowledge immediately, process later Issue critical ## Step 1: Enable in Developer Portal Issue high ## Use a separate deploy script (not on startup) Issue critical ## Never hardcode tokens Issue high ## Generate correct invite URL Issue medium ## Development: Use guild commands Issue medium ## Never block the event loop Issue medium ## Show modal immediately