Build an anime Discord bot

A Discord bot with an /anime slash command that searches the AnimeDoc catalog and replies with an embed: cover, year, rating, genres and episode count.

What you need

  • Node.js 18 or newer.
  • A Discord application with a bot user, from the Discord Developer Portal. Copy its token and application ID, and invite it with the bot and applications.commands scopes.
  • An AnimeDoc API key. Get one free.
mkdir anime-bot && cd anime-bot
npm init -y
npm install discord.js

1. Register the slash command

register.mjs
import { REST, Routes, SlashCommandBuilder } from "discord.js";

const command = new SlashCommandBuilder()
  .setName("anime")
  .setDescription("Search for an anime")
  .addStringOption((o) => o.setName("title").setDescription("Anime title").setRequired(true));

const rest = new REST().setToken(process.env.DISCORD_TOKEN);
await rest.put(Routes.applicationCommands(process.env.DISCORD_APP_ID), { body: [command.toJSON()] });
console.log("Registered /anime");
DISCORD_TOKEN=... DISCORD_APP_ID=... node register.mjs

2. The bot

bot.mjs
import { Client, EmbedBuilder, Events, GatewayIntentBits } from "discord.js";

const API = "https://api.animedoc.cloud/api/v2";
const headers = { "X-API-Key": process.env.ANIMEDOC_API_KEY, "Content-Type": "application/json" };

async function animedoc(path, init = {}) {
  const res = await fetch(API + path, { ...init, headers });
  if (res.status === 429) throw new Error("Too many requests right now, try again shortly.");
  if (!res.ok) throw new Error("AnimeDoc returned " + res.status);
  return (await res.json()).data;
}

const client = new Client({ intents: [GatewayIntentBits.Guilds] });

client.on(Events.InteractionCreate, async (interaction) => {
  if (!interaction.isChatInputCommand() || interaction.commandName !== "anime") return;
  await interaction.deferReply();
  try {
    const title = interaction.options.getString("title", true);
    const { items = [] } = await animedoc("/anime/search/", {
      method: "POST",
      body: JSON.stringify({ keyword: title, page: 1, perPage: 1 }),
    });
    if (!items.length) return interaction.editReply("Nothing found for **" + title + "**.");

    const item = items[0];
    const detail = await animedoc("/anime/detail/?detailPath=" + encodeURIComponent(item.detailPath));
    const seasons = detail.resource?.seasons ?? [];
    const episodes = seasons.reduce((n, s) => n + s.maxEp, 0);

    const embed = new EmbedBuilder()
      .setTitle(item.title)
      .setDescription((item.description || "").slice(0, 400))
      .setThumbnail(item.cover?.url ?? null)
      .addFields(
        { name: "Year", value: (item.releaseDate || "?").slice(0, 4), inline: true },
        { name: "Rating", value: item.imdbRatingValue || "–", inline: true },
        { name: "Episodes", value: episodes ? String(episodes) : "Movie", inline: true },
        { name: "Genres", value: item.genre || "–" },
      )
      .setFooter({ text: "Powered by AnimeDoc" });
    await interaction.editReply({ embeds: [embed] });
  } catch (err) {
    await interaction.editReply(err.message);
  }
});

client.once(Events.ClientReady, (c) => console.log("Logged in as " + c.user.tag));
client.login(process.env.DISCORD_TOKEN);

3. Run it

DISCORD_TOKEN=... ANIMEDOC_API_KEY=ad_live_... node bot.mjs

In your server, type /anime title: one piece.

Limits and next steps

  • Each /anime call uses two requests (search + details). The Free plan's 20 a day covers testing; most servers want Starter.
  • The "Powered by AnimeDoc" footer is the attribution the Free plan requires.
  • Add autocomplete to the title option with Search Suggestions.