| name | discord-api |
| description | [Applies to: **/*] Definitive guidelines for building robust, scalable, and compliant Discord applications using the discord-api, emphasizing REST over Gateway, strong typing, and modern interaction patterns. |
| source | cursor_mdc |
discord-api Best Practices
Building Discord applications requires adherence to specific patterns for reliability and compliance. This guide outlines the essential practices for our team.
1. API Interaction: REST First, Gateway for Events
Always prefer the HTTP REST API for creating, updating, or deleting resources. The Gateway is exclusively for receiving real-time events. This minimizes rate-limit issues and simplifies scaling.
❌ BAD: Using Gateway for resource modification
gateway.send({ op: GatewayOpcodes.MessageCreate, d: { channel_id: '...', content: '...' } });
✅ GOOD: Using REST for resource modification
import { Routes, REST } from 'discord-api-types/v10';
const rest = new REST().setToken(process.env.DISCORD_BOT_TOKEN!);
await rest.post(Routes.channelMessages('12345'), {
body: { content: 'Hello via REST!' },
});
2. Strong Typing with discord-api-types
Ensure all API payloads conform to Discord's official types. This prevents runtime errors and future-proofs against API changes.
❌ BAD: Untyped payloads
const messagePayload = {
channel_id: '123',
content: 'Untyped message',
};
✅ GOOD: Typed payloads
import { APIChannel, APIMessage } from 'discord-api-types/v10';
const channel: APIChannel = await rest.get(Routes.channel('123'));
const messagePayload: APIMessage = {
channel_id: channel.id,
content: 'Typed message!',
};
3. Rate Limit Handling: Exponential Back-off
Discord's API is heavily rate-limited. Implement robust exponential back-off with jitter to handle 429 Too Many Requests responses gracefully.
❌ BAD: Ignoring rate limits or simple retries
try {
await rest.post(Routes.channelMessages('...'), { body: { content: '...' } });
} catch (error: any) {
if (error.status === 429) {
console.warn('Rate limited! Retrying immediately...');
await rest.post(Routes.channelMessages('...'), { body: { content: '...' } });
}
}
✅ GOOD: Exponential back-off with Retry-After
import { setTimeout } from 'node:timers/promises';
async function makeRequestWithRetry(requestFn: () => Promise<any>, retries = 5, delay = 1000): Promise<any> {
try {
return await requestFn();
} catch (error: any) {
if (error.status === 429 && retries > 0) {
const retryAfter = (error.headers?.get('Retry-After') ? parseInt(error.headers.get('Retry-After')!) * 1000 : delay) + Math.random() * 500;
console.warn(`Rate limited. Retrying in ${retryAfter}ms. Retries left: ${retries}`);
await setTimeout(retryAfter);
return makeRequestWithRetry(requestFn, retries - 1, delay * 2);
}
throw error;
}
}
await ( rest.(.(), { : { : } }));
4. Secure Token Management
Never hardcode or commit bot tokens. Use environment variables or a secure secret management system.
❌ BAD: Hardcoded token
const rest = new REST().setToken('YOUR_SUPER_SECRET_TOKEN_HERE');
✅ GOOD: Environment variable
const rest = new REST().setToken(process.env.DISCORD_BOT_TOKEN!);
5. Gateway Intents: Request Only What's Needed
Specify only the Gateway Intents your bot genuinely requires. Over-requesting intents can lead to verification denial and unnecessary resource consumption.
❌ BAD: Requesting all intents or privileged intents without justification
const client = new Client({ intents: [
GatewayIntentBits.Guilds,
GatewayIntentBits.GuildMembers,
GatewayIntentBits.GuildMessages,
GatewayIntentBits.MessageContent,
]});
✅ GOOD: Minimal, justified intents
import { Client, GatewayIntentBits } from 'discord.js';
const client = new Client({ intents: [
GatewayIntentBits.Guilds,
GatewayIntentBits.GuildMessages,
GatewayIntentBits.MessageContent,
]});
6. Modern UI: Slash Commands & Message Components
Prioritize Application Commands (slash commands) and Message Components (buttons, select menus) for user interaction. Text commands are largely deprecated for new features.
❌ BAD: Relying solely on prefix commands
client.on('messageCreate', message => {
if (message.content.startsWith('!mycommand')) {
}
});
✅ GOOD: Registering and handling slash commands
import { ApplicationCommandType, Routes, REST } from 'discord-api-types/v10';
const commands = [{
name: 'ping',
description: 'Replies with Pong!',
type: ApplicationCommandType.ChatInput,
}];
const rest = new REST().setToken(process.env.DISCORD_BOT_TOKEN!);
await rest.put(Routes.applicationCommands(process.env.DISCORD_CLIENT_ID!), { body: commands });
client.on('interactionCreate', async interaction => {
if (!interaction.isChatInputCommand()) return;
if (interaction.commandName === 'ping') {
await interaction.reply('Pong!');
}
});
7. Code Organization: Modular and Testable
Structure your bot into modular, testable components (e.g., separate files for commands, events, services). This improves maintainability and allows for easier testing.
❌ BAD: Monolithic bot file
client.on('ready', () => { });
client.on('messageCreate', () => { });
✅ GOOD: Modular structure
import { Client } from 'discord.js';
import { registerCommands } from './commands';
import { registerEvents } from './events';
const client = new Client({ intents: [...] });
registerCommands(client);
registerEvents(client);
client.login(process.env.DISCORD_BOT_TOKEN);
export function registerCommands(client: Client) {
}
export function registerEvents(client: Client) {
}
8. Data Storage: Use Databases
For persistent data, always use a proper database (SQL or NoSQL). Avoid JSON files for anything beyond simple configuration. JSON files are inefficient for frequent writes and concurrent access.
❌ BAD: Storing user data in JSON files
✅ GOOD: Using a database
import { PrismaClient } from '@prisma/client';
const prisma = new PrismaClient();
async function updateUserPoints(userId: string, points: number) {
await prisma.user.upsert({
where: { id: userId },
update: { points: { increment: points } },
create: { id: userId, points: points, level: 1 },
});
}
9. Game SDK & Rich Presence: Modern Approach
The original Game SDK is archived. For new game integrations, prioritize the Discord Social SDK. If using the legacy Game SDK, rely on its "stub" libraries to ensure compatibility. For Rich Presence, keep strings concise, actionable, and utilize all available fields for maximum impact.
❌ BAD: Long, uninformative Rich Presence strings
✅ GOOD: Concise and actionable Rich Presence