MaShowMagic
Blog

Fastify + Discord.js, cleanly

MaShowBot ไม่ได้มีแค่ Discord commands — มันมี REST API ด้วย สำหรับ dashboard, webhook, และ integration ต่างๆ ผมเลือก Fastify เพราะมันเร็ว, type-safe, และ plugin system ดีมาก แต่การรันมันควบคู่กับ Discord.js ใน process เดียวกันต้องทำให้ถูกวิธีครับ

ทำไมถึงไม่แยก process

หลายคนแนะนำให้แยก API server กับ bot เป็นคนละ process แต่สำหรับ MaShowBot มันไม่สมเหตุสมผล เพราะ API ต้องการเข้าถึง client, guild cache, และ queue state โดยตรง ถ้าแยก process ก็ต้องเพิ่ม IPC หรือ shared database ซึ่งซับซ้อนเกินไปสำหรับขนาดของโปรเจกต์นี้

โครงสร้างที่ใช้

เริ่ม Fastify ก่อน Discord client เสมอ แล้วส่ง client เข้าไปผ่าน plugin:

const app = Fastify({ logger: true });

// ส่ง discord client เข้า fastify context
app.decorate('discord', null);

async function start() {
  const client = new Client({ intents: [...] });

  await client.login(process.env.DISCORD_TOKEN);
  app.discord = client;

  await app.register(routes);
  await app.listen({ port: 3000 });
}

ใน route handler เข้าถึง client ได้ตรงๆ:

app.get('/guilds/:id/queue', async (req, reply) => {
  const guild = app.discord.guilds.cache.get(req.params.id);
  if (!guild) return reply.status(404).send({ error: 'Guild not found' });

  const queue = queues.get(guild.id);
  return reply.send({ current: queue?.current ?? null, tracks: queue?.tracks ?? [] });
});

จัดการ shutdown ให้สะอาด

สิ่งที่คนมักลืมคือ graceful shutdown — ถ้า process ถูก kill กลางคัน connection อาจค้างไว้

process.on('SIGINT', async () => {
  await app.close();
  client.destroy();
  process.exit(0);
});
Fastify ปิด server ก่อน แล้วค่อย destroy Discord client — ลำดับนี้สำคัญมาก มิฉะนั้น in-flight request อาจโดน Discord event ขัดจังหวะ

สิ่งที่ได้จาก Fastify


การรวม Fastify กับ Discord.js ใน process เดียวใช้งานได้ดีมากถ้าจัดการ lifecycle ให้ถูก ถ้าใครกำลังทำ bot ที่มี API ด้วยลองดู approach นี้ครับ แวะมาคุยที่ Discord ได้เลย