Membuat Discord Bot dengan Claude Code: panduan praktis discord.js
Bangun Discord Bot support dengan Claude Code: slash command, permission, env vars, run lokal, dan checklist deploy.
Discord Bot adalah alur operasional, bukan sekadar demo chat
Discord Bot adalah aplikasi yang masuk ke server Discord dan merespons aksi pengguna. Dalam bahasa sederhana, bot ini adalah meja penerima otomatis di dalam komunitas. Ia bisa menerima permintaan dukungan, mengirim jawaban FAQ singkat, memberi tahu moderator, membuat catatan handoff, atau meneruskan insiden ke channel internal sebelum informasinya tenggelam.
Nilainya bukan hanya “bot bisa ngobrol”. Komunitas yang sehat butuh jalur yang jelas untuk pertanyaan seperti “saya macet”, “di mana panduan setup”, “siapa yang menangani kasus ini”, dan “apakah ini perlu dieskalasi”. Jika semua masuk ke channel umum, konteks cepat hilang. Dengan application commands, proses dukungan menjadi terlihat, berulang, dan lebih mudah diaudit.
Claude Code berguna jika dipakai untuk menghasilkan struktur produksi, bukan hanya potongan client.login(). Bot yang siap dipakai perlu registrasi command, environment variables, batas permission, balasan interaction, error handling, keamanan token, dan checklist deployment. Prompt “buatkan Discord Bot” terlalu kabur. Prompt yang menyebut constraint operasional akan menghasilkan proyek yang lebih mudah direview.
Panduan ini membuat bot dukungan dengan discord.js. Command yang dibuat adalah /support, /faq, dan /handoff. Bot menggunakan slash command, tidak bergantung pada message content intent, menetralkan mention berbahaya, dan pada tahap development mendaftarkan command ke test server. Node.js diasumsikan versi 22.12.0 atau lebih baru sesuai dokumentasi discord.js saat ini.
flowchart LR
A["User runs /support"] --> B["Discord interaction"]
B --> C["discord.js bot"]
C --> D["Ephemeral user reply"]
C --> E["Support channel message"]
E --> F["Moderator handoff"]
Versi pertama sengaja dibuat sederhana. Database, queue, ringkasan LLM, CRM, atau pengecekan status pembayaran bisa ditambahkan nanti. Yang harus stabil dulu adalah loop utama: user menjalankan command, bot memberi balasan privat, dan tim menerima catatan yang bisa ditindaklanjuti.
Application commands dan interactions dengan bahasa sederhana
Discord application commands adalah command native yang muncul di client Discord. Bentuk paling umum adalah slash command seperti /support. Untuk dukungan komunitas, ini lebih baik daripada command lama seperti !help, karena Discord dapat menampilkan nama, deskripsi, opsi, pilihan, dan perilaku permission sebelum user mengirim input.
Interactions adalah event yang diterima aplikasi ketika user menjalankan command, menekan tombol, memakai select menu, atau mengirim modal. Dengan discord.js melalui Gateway, biasanya event ini ditangani di Events.InteractionCreate. Discord juga mendukung endpoint HTTP untuk menerima interactions, tetapi untuk tim kecil Gateway Bot lebih mudah dijalankan lokal dan lebih mudah dilacak lewat log.
Gunakan dokumentasi resmi sebagai sumber utama. Tipe command, aturan nama, perbedaan guild command dan global command, serta proses registrasi dijelaskan di Discord Application Commands. Balasan awal, followup, dan token interaction ada di Receiving and Responding to Interactions. Untuk API library dan kebutuhan Node, lihat discord.js documentation dan discord.js guide.
Permission, env vars, dan arsitektur minimal
Di Developer Portal, buat Discord application, tambahkan user Bot, lalu buat invite URL dengan scopes bot dan applications.commands. Jangan mulai dengan administrator permission. Bot ini hanya perlu melihat channel support dan mengirim pesan ke sana. Command /handoff sebaiknya dibatasi untuk orang dengan permission moderator, misalnya Manage Messages.
| Item | Nilai | Catatan produksi |
|---|---|---|
| Node.js | 22.12.0 atau lebih baru | Kebutuhan current discord.js |
| OAuth2 scopes | bot, applications.commands | Dibutuhkan untuk bot dan slash command |
| Bot permissions | View Channels, Send Messages | Tambah hanya setelah review |
DISCORD_TOKEN | Bot token | Jangan commit, screenshot, atau log |
DISCORD_CLIENT_ID | Application ID | Dipakai saat registrasi command |
DISCORD_GUILD_ID | ID test server | Dipakai untuk guild command |
SUPPORT_CHANNEL_ID | Channel support internal | Bot harus bisa mengirim pesan |
Prompt untuk Claude Code sebaiknya spesifik: “Buat bot support Node.js 22 dengan discord.js. Bot punya /support, /faq, dan /handoff, memakai .env, mendaftarkan guild commands saat development, memakai minimum permissions, membalas user dengan ephemeral replies, dan membuat deployment checklist.” Prompt seperti ini menghindari scaffold chatbot generik.
Untuk disiplin teknis yang sama, baca juga manajemen environment variables, pola error handling, dan code review checklist. Bot kecil tetap perlu kebiasaan produksi yang benar.
Starter discord.js yang bisa dijalankan
Contoh ini memakai JavaScript ES modules agar bisa dicoba tanpa konfigurasi TypeScript. Jika DISCORD_GUILD_ID diisi, command didaftarkan ke server test. Jika tidak ada, command menjadi global. Gunakan DEPLOY_COMMANDS=true saat setup lokal; di produksi, registrasi command sebaiknya menjadi langkah deploy yang terkendali.
mkdir discord-support-bot
cd discord-support-bot
npm init -y
npm install discord.js dotenv
mkdir src
Tambahkan type dan start ke package.json.
{
"type": "module",
"scripts": {
"start": "node src/bot.js"
},
"dependencies": {
"discord.js": "latest",
"dotenv": "latest"
}
}
Buat .env.
DISCORD_TOKEN=replace_with_bot_token
DISCORD_CLIENT_ID=replace_with_application_id
DISCORD_GUILD_ID=replace_with_test_guild_id
SUPPORT_CHANNEL_ID=replace_with_support_channel_id
DEPLOY_COMMANDS=true
Buat src/bot.js.
import "dotenv/config";
import {
Client,
Events,
GatewayIntentBits,
MessageFlags,
PermissionFlagsBits,
REST,
Routes,
SlashCommandBuilder,
} from "discord.js";
const token = process.env.DISCORD_TOKEN;
const clientId = process.env.DISCORD_CLIENT_ID;
const guildId = process.env.DISCORD_GUILD_ID;
const supportChannelId = process.env.SUPPORT_CHANNEL_ID;
for (const [name, value] of Object.entries({ token, clientId, supportChannelId })) {
if (!value) throw new Error(`${name} is required.`);
}
const commands = [
new SlashCommandBuilder()
.setName("support")
.setDescription("Send a support request to the team")
.addStringOption((option) =>
option
.setName("summary")
.setDescription("What happened?")
.setMaxLength(900)
.setRequired(true),
)
.addStringOption((option) =>
option
.setName("severity")
.setDescription("How urgent is it?")
.setRequired(true)
.addChoices(
{ name: "low", value: "low" },
{ name: "normal", value: "normal" },
{ name: "high", value: "high" },
),
)
.addStringOption((option) =>
option
.setName("context")
.setDescription("Steps, links, or error messages")
.setMaxLength(1500),
),
new SlashCommandBuilder()
.setName("faq")
.setDescription("Show a short answer for a common topic")
.addStringOption((option) =>
option
.setName("topic")
.setDescription("FAQ topic")
.setRequired(true)
.addChoices(
{ name: "setup", value: "setup" },
{ name: "permissions", value: "permissions" },
{ name: "rollout", value: "rollout" },
),
),
new SlashCommandBuilder()
.setName("handoff")
.setDescription("Create a moderator handoff note")
.setDefaultMemberPermissions(PermissionFlagsBits.ManageMessages)
.addUserOption((option) =>
option.setName("target").setDescription("User to hand off").setRequired(true),
)
.addStringOption((option) =>
option
.setName("note")
.setDescription("What should the next moderator know?")
.setMaxLength(1500)
.setRequired(true),
),
].map((command) => command.toJSON());
const client = new Client({ intents: [GatewayIntentBits.Guilds] });
client.once(Events.ClientReady, (readyClient) => {
console.log(`Logged in as ${readyClient.user.tag}`);
});
client.on(Events.InteractionCreate, async (interaction) => {
if (!interaction.isChatInputCommand()) return;
try {
if (!interaction.inGuild()) {
await interaction.reply({
content: "Please use this command inside the server.",
flags: MessageFlags.Ephemeral,
});
return;
}
if (interaction.commandName === "support") await handleSupport(interaction);
else if (interaction.commandName === "faq") await handleFaq(interaction);
else if (interaction.commandName === "handoff") await handleHandoff(interaction);
else await safeReply(interaction, "Unknown command.");
} catch (error) {
console.error("Interaction failed:", error);
await safeReply(interaction, "Something went wrong. Please contact a moderator.");
}
});
async function handleSupport(interaction) {
const summary = interaction.options.getString("summary", true);
const severity = interaction.options.getString("severity", true);
const context = interaction.options.getString("context") ?? "No extra context.";
const channel = await fetchSupportChannel();
await channel.send({
content: [
"**New support request**",
`Reporter: ${interaction.user.tag} (${interaction.user.id})`,
`Severity: ${severity}`,
`Channel: <#${interaction.channelId}>`,
`Summary: ${neutralizeMentions(summary)}`,
`Context: ${neutralizeMentions(context)}`,
].join("\n"),
allowedMentions: { parse: [] },
});
await interaction.reply({
content: "Thanks. Your request was sent to the support team.",
flags: MessageFlags.Ephemeral,
});
}
async function handleFaq(interaction) {
const topic = interaction.options.getString("topic", true);
const answers = {
setup: "Install Node.js 22.12+, invite the bot with bot and applications.commands scopes, then run npm start.",
permissions: "Start with View Channels and Send Messages. Reserve Manage Messages for moderator-only commands.",
rollout: "Use guild commands for testing. Promote to global commands only after rollback and logging are checked.",
};
await interaction.reply({
content: answers[topic],
flags: MessageFlags.Ephemeral,
});
}
async function handleHandoff(interaction) {
if (!interaction.memberPermissions?.has(PermissionFlagsBits.ManageMessages)) {
await interaction.reply({
content: "You need Manage Messages permission to use this command.",
flags: MessageFlags.Ephemeral,
});
return;
}
const target = interaction.options.getUser("target", true);
const note = interaction.options.getString("note", true);
const channel = await fetchSupportChannel();
await channel.send({
content: [
"**Moderator handoff**",
`Target: ${target.tag} (${target.id})`,
`From: ${interaction.user.tag} (${interaction.user.id})`,
`Note: ${neutralizeMentions(note)}`,
].join("\n"),
allowedMentions: { parse: [] },
});
await interaction.reply({
content: "Handoff note created.",
flags: MessageFlags.Ephemeral,
});
}
async function fetchSupportChannel() {
const channel = await client.channels.fetch(supportChannelId);
if (!channel || !channel.isTextBased() || typeof channel.send !== "function") {
throw new Error("SUPPORT_CHANNEL_ID must be a text channel the bot can send to.");
}
return channel;
}
function neutralizeMentions(value) {
return value
.replaceAll("@everyone", "@ everyone")
.replaceAll("@here", "@ here")
.replace(/<@!?(\d+)>/g, "user:$1")
.replace(/<@&(\d+)>/g, "role:$1");
}
async function safeReply(interaction, content) {
const payload = { content, flags: MessageFlags.Ephemeral };
if (interaction.replied || interaction.deferred) await interaction.followUp(payload);
else await interaction.reply(payload);
}
async function deployCommands() {
const rest = new REST({ version: "10" }).setToken(token);
const route = guildId
? Routes.applicationGuildCommands(clientId, guildId)
: Routes.applicationCommands(clientId);
await rest.put(route, { body: commands });
console.log(guildId ? "Guild commands deployed." : "Global commands deployed.");
}
if (process.env.DEPLOY_COMMANDS === "true") {
await deployCommands();
}
await client.login(token);
Di lokal, jalankan node --version dan pastikan 22.12.0 atau lebih baru. Setelah itu jalankan npm start. Coba /support lebih dulu: user harus mendapat balasan privat, sedangkan channel internal menerima pesan terstruktur. Lalu coba /faq. Terakhir, jalankan /handoff dengan akun moderator dan akun biasa untuk memastikan batas permission bekerja.
Use case: tiga workflow yang membuat bot layak dipakai
Use case pertama adalah support intake. Command yang baik tidak perlu dua puluh field. Summary, severity, dan context sudah cukup untuk triage, yaitu memutuskan apakah kasus harus dijawab, direproduksi, ditugaskan, atau dieskalasi. Di server uji kecil, format ini mengurangi tanya-jawab ulang karena pesan pertama sudah membawa urgensi, gejala, dan petunjuk reproduksi.
Use case kedua adalah routing FAQ. Bot tidak perlu menempelkan satu halaman dokumentasi ke chat. Lebih baik beri jawaban pendek dan link yang tepat. Masalah setup bisa diarahkan ke panduan mulai Claude Code, pertanyaan CLI ke pengembangan tool CLI, dan aturan tim ke template CLAUDE.md. Dengan begitu bot menjadi navigasi situs, bukan dokumentasi kedua.
Use case ketiga adalah handoff moderator. Komunitas berbayar, cohort training, server game, dan support produk punya pergantian orang. Pesan “tolong ada yang cek” cepat kehilangan konteks. /handoff menyimpan target user, pengirim catatan, dan informasi yang perlu diketahui moderator berikutnya.
Skenario keempat adalah dukungan training. Dalam workshop Claude Code, banyak peserta tersangkut pada versi Node, env var, atau command error yang sama. Jika bot mengumpulkan versi, command yang dijalankan, dan beberapa baris log terakhir, instruktur bisa mulai dari diagnosis. Untuk membawa alur ini ke server tim, CTA yang tepat adalah training dan konsultasi.
Pitfall: kesalahan produksi yang harus dibuang sejak awal
Pitfall pertama adalah token bot bocor. Jika token muncul di Git, screenshot, CI log, atau dokumentasi, anggap sudah kompromi. Rotasi token di Developer Portal dan hapus nilai lama. Saat memakai Claude Code, minta .env.example dengan placeholder saja dan larang output secret asli.
Pitfall kedua adalah terus memakai global commands selama development. Guild commands lebih cepat dan hanya berlaku di server test. Global commands adalah permukaan produksi. Promosikan setelah nama, deskripsi, permission, dan rollback jelas. Hindari juga registrasi ulang command di setiap restart produksi kecuali itu langkah deploy yang memang direview.
Pitfall ketiga adalah cabang interaction tanpa balasan. User akan menganggap command rusak. Semua jalur harus reply, defer, atau follow up, termasuk error dan command tidak dikenal. Jika nanti menambah API eksternal atau ringkasan LLM, defer dulu lalu kirim hasilnya.
Pitfall keempat adalah penyalahgunaan mention. Jika input user diteruskan mentah ke channel internal, @everyone, @here, mention user, dan mention role bisa membuat insiden notifikasi. Contoh memakai allowedMentions: { parse: [] } dan pembersihan teks. Pertahankan keduanya.
Pitfall kelima adalah memberi administrator permission karena praktis. Ini menyembunyikan keputusan permission dan memperbesar dampak jika token dicuri. Mulai dari View Channels dan Send Messages. Tambahkan permission hanya jika alasannya bisa dijelaskan saat review.
Checklist keamanan dan deployment
- Ada pemilik yang bisa merotasi token di Developer Portal
.envtidak masuk Git dan.env.examplehanya berisi placeholder- Invite URL dimulai dengan scopes
botdanapplications.commands - Bot permissions dimulai dari View Channels dan Send Messages
/handoffdibatasi untuk moderator- Guild commands diuji sebelum global commands
- Semua jalur interaction melakukan reply, defer, atau follow up
- Input user tidak bisa memicu mention
- Log tidak berisi token, data pembayaran, email, atau data support privat
- Hosting memakai Node.js 22.12.0 atau lebih baru
- Langkah restart dan rollback sudah ditulis
Bot kecil bisa berjalan di Railway, Render, Fly.io, VPS, atau server internal. Platform tidak sepenting tiga hal: secret terpisah dari kode, log bisa dilihat, dan restart terdokumentasi. Minta Claude Code menulis README dengan bagian-bagian itu secara eksplisit.
Untuk template yang bisa dipakai ulang, lihat produk ClaudeCodeLab. Jika tim ingin merancang support Discord, adopsi Claude Code, permission, review, dan training, mulai dari training dan konsultasi. Agar bot dirawat sebagai fitur nyata, hubungkan juga dengan otomasi code review.
Hasil setelah dicoba
Saat starter ini dijalankan di server kecil, peningkatan utama bukan kecerdasan bot. Peningkatannya ada pada bentuk permintaan. Pesan bebas sering dimulai dengan “tidak jalan”. /support memaksa summary, severity, dan context, sehingga balasan pertama moderator lebih dekat ke diagnosis daripada permintaan detail tambahan. Area paling berisiko adalah token, global commands, mentions, dan permissions. Claude Code paling berguna ketika diminta membuat kode sekaligus .env.example, tabel permission, balasan error, dan checklist deploy.
PDF gratis: cheatsheet Claude Code
Masukkan email dan unduh satu halaman berisi command, kebiasaan review, dan workflow aman.
Kami menjaga datamu dan tidak mengirim spam.
Tentang penulis
Masa
Engineer yang berfokus pada workflow Claude Code praktis dan adopsi tim.
Artikel terkait
Workflow Obsidian ke CLAUDE.md untuk Claude Code
Ubah catatan kerja Obsidian menjadi operating note CLAUDE.md agar konteks tidak dijelaskan ulang.
Claude Code Revenue CTA Routing: dari artikel ke PDF, Gumroad, dan konsultasi
Workflow Claude Code untuk mengarahkan pembaca ke PDF gratis, Gumroad, atau konsultasi sesuai intent.
Aturan handoff tim Claude Code: bukti review, permission, rollback, dan jalur revenue
Format handoff Claude Code untuk tim: bukti, permission rule, rollback, PDF gratis, Gumroad, dan konsultasi.