Skip to content

gonzyui/ani-client

Repository files navigation

ani-client

CI npm version npm downloads codecov TypeScript License: MIT

A fully typed, zero-dependency client for the AniList GraphQL API.

Showcase: See who's using ani-client

Highlights

  • Zero dependencies — uses the native fetch API
  • Universal — Node.js ≥ 20, Bun, Deno, and modern browsers
  • Dual format — ships ESM + CJS with full .d.ts declarations
  • LRU cache with TTL, stale-while-revalidate, and hit/miss stats
  • Rate-limit protection with exponential backoff, retries, and custom strategies
  • Request deduplication — concurrent identical queries share a single in-flight request
  • Batch queries — fetch up to 50 media/characters/staff in one API call
  • Auto-pagination — async iterator that yields items across pages
  • AbortSignal support — cancel globally or per-request with withSignal()
  • Injectable logger — plug in console, pino, winston, or any compatible logger
  • Redis-ready — swap the cache adapter with the built-in RedisCache for distributed setups

Install

npm install ani-client
# or
pnpm add ani-client
# or
yarn add ani-client

Quick start

import { AniListClient, MediaType } from "ani-client";

const client = new AniListClient();

// Fetch an anime by AniList ID
const bebop = await client.getMedia(1);
console.log(bebop.title.romaji); // "Cowboy Bebop"

// Search with filters
const results = await client.searchMedia({
  query: "Naruto",
  type: MediaType.ANIME,
  genres: ["Action"],
  perPage: 10,
});

// Cross-platform lookup by MyAnimeList ID
const fma = await client.getMediaByMalId(5114);

Features at a glance

Caching & stale-while-revalidate

const client = new AniListClient({
  cache: {
    ttl: 1000 * 60 * 5,               // 5 min TTL
    maxSize: 200,                      // LRU capacity
    staleWhileRevalidateMs: 60_000,    // serve stale for 1 min after expiry
  },
});

// Check cache performance
console.log(client.cacheStats);
// { hits: 42, misses: 8, stales: 2, hitRate: 0.84 }

Per-request cancellation

const controller = new AbortController();
const scoped = client.withSignal(controller.signal);

setTimeout(() => controller.abort(), 3_000);
const anime = await scoped.getMedia(1);

Structured logging

const client = new AniListClient({ logger: console });
// debug: "API request"  { query: "query { Media(id: 1) { ... } }" }
// debug: "Request complete" { durationMs: 120, status: 200 }

Rate limiting & retries

const client = new AniListClient({
  rateLimit: {
    maxRequests: 85,
    windowMs: 60_000,
    maxRetries: 3,
    retryOnNetworkError: true,
    retryStrategy: (attempt) => (attempt + 1) * 1000, // linear backoff
  },
});

console.log(client.rateLimitInfo);
// { remaining: 82, limit: 85, reset: 1741104000 }

Batch & pagination

// Fetch 100 anime in 2 API calls (50 per batch)
const batch = await client.getMediaBatch([1, 2, 3, /* ...up to 100 IDs */]);

// Auto-paginate through all results
for await (const anime of client.paginate(
  (page) => client.searchMedia({ query: "Gundam", page, perPage: 50 }),
  5, // max 5 pages
)) {
  console.log(anime.title.romaji);
}

Users, characters, studios & more

const user = await client.getUser("AniList");
const favs = await client.getUserFavorites("AniList", { perPage: 50 });
const char = await client.getCharacter(1, { voiceActors: true });
const studio = await client.getStudio(21, { media: { perPage: 50 } });
const schedule = await client.getWeeklySchedule();

Documentation

Full API reference, guides (caching, pagination, includes, hooks, etc.) and configuration examples:

ani-client.js.org

Requirements

Runtime Version
Node.js ≥ 20
Bun ≥ 1.0
Deno ≥ 1.28
Browsers Any with fetch + AbortController

Contributing

See CONTRIBUTING.md for development setup, coding standards, and PR guidelines.

License

MIT © gonzyui