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
botandapplications.commandsscopes. - An AnimeDoc API key. Get one free.
mkdir anime-bot && cd anime-bot
npm init -y
npm install discord.js1. Register the slash command
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.mjs2. The bot
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.mjsIn 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.