Bir Discord botu büyüdükçe tüm komutları tek bir dev index.js dosyasında tutmak hızla kâbusa döner. İyi tasarlanmış bir discord komut handler, her komutu ve her event'i kendi dosyasında tutar; bot açılırken bu dosyaları klasörlerden tarayıp otomatik yükler. Sonuç: yeni bir özellik eklemek için tek yapman gereken bir dosya oluşturmaktır — ana dosyaya tek satır bile dokunmazsın. Bu yazıda discord.js v14 ile dosya bazlı, modüler ve gerçekten ölçeklenebilir bir yapı kuracağız.
Neden dosya bazlı bir handler?
Küçük bir bot için if (komut === 'ping') zinciri yeterli görünebilir. Ama 30-40 komuta ulaştığında bu yaklaşım çöker: dosya yüzlerce satır olur, çakışmalar artar, ekip çalışması imkânsızlaşır. Modüler bir handler şu avantajları getirir:
- Sorumluluk ayrımı: Her komut tek bir işten sorumlu, kendi dosyasında yaşar.
- Otomatik keşif: Yeni komut dosyası eklediğinde handler onu kendiliğinden bulur.
- Test edilebilirlik: Komutlar saf fonksiyonlara yaklaştığı için izole test etmek kolaylaşır.
- Ekip dostu: İki kişi farklı komutlar üzerinde aynı anda çalışsa bile çakışma yaşamaz.
Proje yapısı
İyi bir dizin düzeni handler'ın yarısıdır. Komutları kategorilere göre alt klasörlere bölmek hem düzeni hem de ölçeklenmeyi kolaylaştırır:
src/
├─ index.js # giriş noktası, client'ı başlatır
├─ handlers/
│ ├─ commands.js # komutları yükler
│ └─ events.js # event'leri yükler
├─ commands/
│ ├─ utility/
│ │ └─ ping.js
│ └─ moderation/
│ └─ ban.js
└─ events/
├─ ready.js
└─ interactionCreate.js
Tek bir komutun anatomisi
Her komut dosyası aynı sözleşmeye uyar: bir data (Slash komut tanımı) ve bir execute fonksiyonu dışa aktarır. Bu tutarlılık, handler'ın komutları tek tip muamele edebilmesini sağlar.
// commands/utility/ping.js
const { SlashCommandBuilder } = require('discord.js');
module.exports = {
data: new SlashCommandBuilder()
.setName('ping')
.setDescription('Botun gecikmesini ölçer'),
async execute(interaction) {
await interaction.reply(`Pong! ${interaction.client.ws.ping}ms`);
},
};
Komutları otomatik yüklemek
Handler, commands/ klasörünü ve alt klasörlerini gezer, her dosyayı require eder ve geçerli olanları bir Collection içinde saklar. Node.js'in yerleşik fs ve path modülleri bu iş için yeterlidir.
// handlers/commands.js
const { Collection } = require('discord.js');
const fs = require('node:fs');
const path = require('node:path');
module.exports = (client) => {
client.commands = new Collection();
const root = path.join(__dirname, '..', 'commands');
for (const folder of fs.readdirSync(root)) {
const dir = path.join(root, folder);
const files = fs.readdirSync(dir).filter((f) => f.endsWith('.js'));
for (const file of files) {
const command = require(path.join(dir, file));
if ('data' in command && 'execute' in command) {
client.commands.set(command.data.name, command);
} else {
console.warn(`[UYARI] ${file} eksik "data" veya "execute".`);
}
}
}
};
Dikkat edilmesi gereken nokta şu if ('data' in command && 'execute' in command) kontrolüdür: eksik bir dosya botun komple çökmesine değil, yalnızca bir uyarıya yol açar.
Event'leri aynı mantıkla yüklemek
Komutlar için kurduğumuz desen event'lerde de birebir geçerli. Her event dosyası bir name, isteğe bağlı bir once bayrağı ve bir execute dışa aktarır. Handler bunları client.on ya da client.once ile bağlar.
// events/ready.js
const { Events } = require('discord.js');
module.exports = {
name: Events.ClientReady,
once: true,
execute(client) {
console.log(`Giriş yapıldı: ${client.user.tag}`);
},
};
// handlers/events.js
const fs = require('node:fs');
const path = require('node:path');
module.exports = (client) => {
const dir = path.join(__dirname, '..', 'events');
const files = fs.readdirSync(dir).filter((f) => f.endsWith('.js'));
for (const file of files) {
const event = require(path.join(dir, file));
if (event.once) client.once(event.name, (...a) => event.execute(...a));
else client.on(event.name, (...a) => event.execute(...a));
}
};
Komutları çalıştıran köprü: interactionCreate
Slash komutları aslında birer "interaction"dır. Tek bir interactionCreate event'i gelen etkileşimi alır, client.commands içinden ilgili komutu bulur ve execute'ını çağırır. Hataları burada merkezî olarak yakalamak önemlidir.
// events/interactionCreate.js
const { Events } = require('discord.js');
module.exports = {
name: Events.InteractionCreate,
async execute(interaction) {
if (!interaction.isChatInputCommand()) return;
const command = interaction.client.commands.get(interaction.commandName);
if (!command) return;
try {
await command.execute(interaction);
} catch (err) {
console.error(err);
const msg = { content: 'Komut çalışırken bir hata oluştu.', ephemeral: true };
if (interaction.replied || interaction.deferred) {
await interaction.followUp(msg);
} else {
await interaction.reply(msg);
}
}
},
};
Slash komutlarını Discord'a kaydetmek
Önemli bir ayrım: komut dosyalarını yüklemek botun onları tanımasını sağlar, ama Discord arayüzünde görünmeleri için komutların Discord API'sine ayrıca kaydedilmesi (deploy) gerekir. Bunu genelde ayrı bir deploy-commands.js betiğiyle yaparsın. Geliştirme sırasında tek bir sunucuya kaydetmek anında güncellenir; global kayıt ise yayılması bir saate kadar sürebilir.
const { REST, Routes } = require('discord.js');
// commands dizinini gezip her command.data.toJSON()'ı bir diziye topla
const rest = new REST().setToken(process.env.TOKEN);
await rest.put(
Routes.applicationGuildCommands(CLIENT_ID, GUILD_ID),
{ body: commands },
);
Her şeyi index.js'te birleştirmek
// index.js
require('dotenv').config();
const { Client, GatewayIntentBits } = require('discord.js');
const client = new Client({ intents: [GatewayIntentBits.Guilds] });
require('./handlers/commands')(client);
require('./handlers/events')(client);
client.login(process.env.TOKEN);
Gördüğün gibi giriş dosyası artık sade ve değişmez. Yeni bir komut mu lazım? commands/ altına bir dosya ekle. Yeni bir event mi? events/ altına bir dosya ekle. Mimarinin güzelliği tam olarak budur: çekirdek sabit kalırken özellikler sınırsızca eklenebilir.
Sık Sorulan Sorular
Mesaj komutları mı, slash komutları mı kullanmalıyım?
Yeni botlar için slash komutları (uygulama komutları) tercih edilmelidir; keşfedilebilir, doğrulanmış parametreler sunar ve Discord'un mesaj içeriği yönelimini gerektirmez. Aynı handler deseni iki türü de destekleyebilir, ama yeni projelerde slash komutlarına odaklanmak en sağlıklısıdır.
Bot çalışırken komutu yeniden yüklemek (hot reload) mümkün mü?
Evet, Node.js'in require önbelleğini silip dosyayı tekrar yükleyerek bir "reload" komutu yazabilirsin. Bu geliştirme sırasında çok pratiktir; yine de yapısal değişikliklerde botu tamamen yeniden başlatmak en güvenli yoldur.
Komut dosyaları neden ortak bir sözleşmeye uymalı?
Her dosyanın aynı data ve execute yapısını dışa aktarması, handler'ın her komuta tek tip davranabilmesini sağlar. Bu tutarlılık, otomatik yükleme, doğrulama ve hata yönetimini mümkün kılan temel sözleşmedir.
Botun büyüdükçe dağılmaya mı başladı? Modüler bir komut handler'ı, temiz event mimarisi ve sağlam bir deploy akışıyla botunu profesyonel bir temele oturtabiliriz. Benimle iletişime geç, projeni birlikte ölçeklenebilir hale getirelim.