Criar um Discord Bot com Claude Code: guia prático com discord.js
Crie um Discord Bot de suporte com Claude Code, slash commands, permissões, variáveis de ambiente e checklist de deploy.
Um Discord Bot deve ser um fluxo operacional, não só uma demonstração
Um Discord Bot é uma aplicação que entra em um servidor Discord e responde a ações dos usuários. Em termos simples, ele funciona como uma recepção automática dentro da comunidade. Ele pode receber pedidos de suporte, responder uma FAQ curta, avisar moderadores, preparar uma nota de handoff ou encaminhar um incidente para um canal interno antes que a informação se perca.
Isso é mais útil do que um bot que apenas conversa. Uma comunidade saudável precisa de caminhos claros para “estou bloqueado”, “onde está o guia”, “quem assume este caso” e “isso precisa de escalonamento”. Se tudo fica em um canal geral, as mensagens somem no meio do papo. Com application commands, o processo fica visível, repetível e auditável.
Claude Code ajuda mais quando você pede não apenas um trecho com client.login(), mas um ponto de partida com cuidados reais: registro de comandos, variáveis de ambiente, limites de permissão, respostas de interaction, tratamento de erros, proteção do token e checklist de implantação. Um pedido vago como “faça um Discord Bot” costuma gerar uma demo que só funciona no caminho feliz.
Este guia cria um bot de suporte com discord.js. Ele implementa /support, /faq e /handoff, usa slash commands, não depende do intent de message content, evita mentions perigosas e começa registrando comandos no servidor de teste. O exemplo assume Node.js 22.12.0 ou superior, alinhado à documentação atual do discord.js.
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"]
A decisão central é manter a primeira versão simples e confiável. Banco de dados, fila, resumo com LLM, CRM e verificação de pagamento podem entrar depois. Primeiro, o comando precisa chegar, o usuário precisa receber uma resposta privada e a equipe precisa ver uma mensagem útil.
Application commands e interactions sem complicação
Discord application commands são comandos nativos que aparecem no cliente do Discord. A forma mais comum é o slash command, como /support. Eles são melhores para suporte do que comandos antigos com prefixo, como !help, porque o Discord mostra nome, descrição, opções, escolhas e comportamento de permissão antes do envio.
Interactions são os eventos que sua aplicação recebe quando alguém executa um comando, clica em um botão, usa um menu ou envia um modal. Com discord.js via Gateway, normalmente você trata isso em Events.InteractionCreate. O Discord também permite receber interactions por endpoint HTTP, mas para uma equipe pequena o Bot com Gateway é mais simples de rodar localmente e depurar.
A fonte de verdade deve ser oficial. Tipos de comando, regras de nome, diferenças entre guild commands e global commands e registro estão em Discord Application Commands. Resposta inicial, followups e tokens de interaction estão em Receiving and Responding to Interactions. Para API da biblioteca e requisitos de Node, consulte discord.js documentation e o discord.js guide.
Permissões, env vars e arquitetura mínima
No Developer Portal, crie uma Discord application, adicione um usuário Bot e gere uma URL de convite com os scopes bot e applications.commands. Não comece com permissão de administrador. Este bot precisa ver o canal de suporte e enviar mensagens nele. O comando /handoff deve ser restrito a pessoas com permissão de moderação, por exemplo Manage Messages.
| Item | Valor | Nota de produção |
|---|---|---|
| Node.js | 22.12.0 ou superior | Requisito atual do discord.js |
| OAuth2 scopes | bot, applications.commands | Necessários para bot e slash commands |
| Bot permissions | View Channels, Send Messages | Adicione mais só após revisão |
DISCORD_TOKEN | Bot token | Nunca commit, print ou log |
DISCORD_CLIENT_ID | Application ID | Usado no registro de comandos |
DISCORD_GUILD_ID | ID do servidor de teste | Útil para guild commands |
SUPPORT_CHANNEL_ID | Canal interno de suporte | O bot deve conseguir enviar |
Um prompt bom para Claude Code seria: “Crie um bot de suporte em Node.js 22 com discord.js. Ele deve ter /support, /faq e /handoff, usar .env, registrar guild commands no desenvolvimento, usar permissões mínimas, responder com ephemeral replies e gerar checklist de deploy.” Essa especificidade evita um chatbot genérico.
Para reforçar a base técnica, leia também gerenciamento de variáveis de ambiente, padrões de tratamento de erro e checklist de code review. O bot é pequeno, mas os hábitos são os mesmos de um serviço de produção.
Starter executável com discord.js
O exemplo usa JavaScript com ES modules para ser fácil de copiar sem configurar TypeScript. Se DISCORD_GUILD_ID estiver definido, os comandos são registrados no servidor de teste. Sem ele, os comandos são globais. Use DEPLOY_COMMANDS=true no setup local; em produção, o registro de comandos deve ser uma etapa controlada.
mkdir discord-support-bot
cd discord-support-bot
npm init -y
npm install discord.js dotenv
mkdir src
Adicione type e start ao package.json.
{
"type": "module",
"scripts": {
"start": "node src/bot.js"
},
"dependencies": {
"discord.js": "latest",
"dotenv": "latest"
}
}
Crie .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
Crie 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);
Localmente, rode node --version e confirme 22.12.0 ou superior. Depois execute npm start. Teste primeiro /support: o usuário deve ver uma resposta privada e o canal interno deve receber a mensagem estruturada. Depois teste /faq. Por fim, teste /handoff com uma conta moderadora e outra comum.
Use case: três fluxos que fazem o bot valer a pena
O primeiro Use case é entrada de suporte. Um bom comando não precisa de vinte campos. Summary, severity e context já permitem triage, ou seja, decidir se o caso deve ser respondido, reproduzido, atribuído ou escalado. Em um servidor pequeno de teste, esse formato reduziu perguntas de acompanhamento porque a primeira nota já trazia urgência, sintoma e pistas de reprodução.
O segundo Use case é roteamento de FAQ. O bot não deve colar uma página inteira no chat. Melhor responder curto e linkar certo. Uma dúvida de instalação pode ir para guia inicial de Claude Code, perguntas de CLI para desenvolvimento de ferramentas CLI e regras de equipe para templates CLAUDE.md. O bot vira navegação do site, não uma segunda documentação.
O terceiro Use case é handoff entre moderadores. Comunidades pagas, turmas de treinamento, servidores de jogos e suporte de produto têm troca de turno. “Alguém olha isso?” perde contexto rápido. /handoff registra usuário-alvo, autor e informação que o próximo moderador precisa saber.
Um quarto cenário é suporte em treinamento. Em um workshop de Claude Code, várias pessoas encontram erros parecidos de Node, variáveis de ambiente ou comandos. Se o bot coleta versão, comando executado e últimas linhas do log, o instrutor começa diagnosticando, não pedindo dados básicos. Para levar isso ao servidor de uma equipe, o próximo passo natural é treinamento e consultoria.
Pitfall: falhas de produção para remover cedo
O primeiro Pitfall é vazar o token do bot. Se aparecer em Git, prints, logs de CI ou documentação, trate como comprometido. Rotacione no Developer Portal e remova o valor antigo. Ao usar Claude Code, peça explicitamente .env.example com placeholders e nenhuma impressão de secrets reais.
O segundo problema é registrar global commands durante todo o desenvolvimento. Guild commands são rápidos e limitados ao servidor de teste. Global commands são superfície de produção. Promova só depois de revisar nomes, descrições, permissões e rollback. Também evite registrar comandos a cada reinício de produção sem controle.
O terceiro problema é deixar uma rota de interaction sem resposta. Para o usuário, isso parece comando quebrado. Todo caminho precisa fazer reply, defer ou follow up, incluindo erros e comandos desconhecidos. Se você adicionar API externa ou resumo com LLM, faça defer antes e responda depois.
O quarto problema é abuso de mentions. Se a entrada do usuário for encaminhada sem filtro para o canal interno, @everyone, @here, mentions de usuário e de cargo podem criar incidentes de notificação. O exemplo usa allowedMentions: { parse: [] } e limpeza de texto. Mantenha os dois.
O quinto problema é dar administrator ao bot por conveniência. Isso esconde decisões de permissão e aumenta o impacto se o token for roubado. Comece com View Channels e Send Messages. Adicione permissões apenas quando conseguir explicá-las em revisão.
Checklist de segurança e deploy
- Há uma pessoa responsável por rotacionar o token no Developer Portal
.envnão entra no Git e.env.examplesó tem placeholders- A URL de convite começa com
boteapplications.commands - Bot permissions começam com View Channels e Send Messages
/handoffé restrito a moderadores- Guild commands são testados antes de global commands
- Toda rota de interaction faz reply, defer ou follow up
- A entrada do usuário não dispara mentions
- Logs não contêm token, dados de pagamento, email ou suporte privado
- O ambiente usa Node.js 22.12.0 ou superior
- Reinício e rollback estão documentados
Um bot pequeno pode rodar em Railway, Render, Fly.io, VPS ou servidor interno. A plataforma importa menos do que separar secrets do código, enxergar logs e saber reiniciar. Peça para Claude Code escrever o README com essas seções.
Para modelos reutilizáveis, veja produtos ClaudeCodeLab. Se sua equipe quer estruturar suporte no Discord, adoção de Claude Code, permissões, revisão e treinamento, comece por treinamento e consultoria. Para manter o bot como feature real, conecte também com automação de code review.
Resultado após testar
Ao rodar este starter em um servidor pequeno, o maior ganho não foi a inteligência do bot. Foi o formato da solicitação. Mensagens livres costumam começar com “não funciona”. /support força summary, severity e context, então a primeira resposta do moderador fica mais próxima de diagnóstico do que de pedido de detalhes. As partes perigosas foram token, global commands, mentions e permissões. Claude Code foi mais útil quando pedi código junto com .env.example, tabela de permissões, respostas de erro e checklist de deploy.
PDF grátis: cheatsheet do Claude Code
Informe seu e-mail e baixe uma página com comandos, hábitos de revisão e workflows seguros.
Cuidamos dos seus dados e não enviamos spam.
Sobre o autor
Masa
Engenheiro focado em workflows práticos com Claude Code.
Artigos relacionados
Workflow Obsidian para CLAUDE.md com Claude Code
Transforme notas de trabalho do Obsidian em notas operacionais CLAUDE.md para preservar contexto.
Claude Code Revenue CTA Routing: artigos para PDF, Gumroad e consultoria
Um fluxo com Claude Code para levar leitores ao PDF grátis, Gumroad ou consultoria conforme intenção.
Regras de handoff para equipes com Claude Code: evidências, permissões, rollback e receita
Formato prático para entregar trabalho do Claude Code com prova, permissões, rollback, PDF grátis, Gumroad e consultoria.