ani-client 1.4.2 → 1.4.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -6,48 +6,18 @@
6
6
 
7
7
  > A simple, typed client to fetch anime, manga, character, staff and user data from [AniList](https://anilist.co).
8
8
 
9
+ ✨ **Showcase**: [Check here](https://ani-client-docs.vercel.app/showcase) to see which projects use this package!
10
+
9
11
  - **Zero dependencies** — uses the native `fetch` API
10
12
  - **Universal** — Node.js ≥ 20, Bun, Deno and modern browsers
11
13
  - **Dual format** — ships ESM + CJS with full TypeScript declarations
12
- - **Built-in caching** — in-memory LRU with optional Redis adapter
13
- - **Rate-limit aware** — auto-retry on 429, configurable timeout & network error retry
14
- - **Request deduplication** — identical in-flight requests are coalesced
15
- - **Event hooks** — observe every request, cache hit, retry and response
16
- - **Batch queries** — fetch multiple IDs in a single GraphQL call
14
+ - **Reliable** — Built-in caching, Rate-limit protections, automatic retries & request deduplication!
15
+
16
+ ## 📖 Documentation
17
17
 
18
- ## Table of contents
18
+ The full API reference, usage guide, and configuration examples are available on our official documentation website!
19
19
 
20
- - [Install](#install)
21
- - [Quick start](#quick-start)
22
- - [Client options](#client-options)
23
- - [API reference](#api-reference)
24
- - [Media](#media)
25
- - [Include options](#include-options)
26
- - [Characters](#characters)
27
- - [Staff](#staff)
28
- - [Users](#users)
29
- - [Airing, Chapters & Planning](#airing-chapters--planning)
30
- - [Season charts](#season-charts)
31
- - [User media lists](#user-media-lists)
32
- - [Recommendations](#recommendations)
33
- - [Relations](#relations)
34
- - [Studios](#studios)
35
- - [Genres & Tags](#genres--tags)
36
- - [Batch queries](#batch-queries)
37
- - [Raw queries](#raw-queries)
38
- - [Auto-pagination](#auto-pagination)
39
- - [Caching](#caching)
40
- - [Memory cache](#memory-cache)
41
- - [Redis cache](#redis-cache)
42
- - [Custom adapter](#custom-adapter)
43
- - [Cache invalidation](#cache-invalidation)
44
- - [Rate limiting](#rate-limiting)
45
- - [Request deduplication](#request-deduplication)
46
- - [Event hooks](#event-hooks)
47
- - [Error handling](#error-handling)
48
- - [Types](#types)
49
- - [Contributing](#contributing)
50
- - [License](#license)
20
+ **[👉 View the full documentation here](https://ani-client-docs.vercel.app/)**
51
21
 
52
22
  ## Install
53
23
 
@@ -60,6 +30,9 @@ pnpm add ani-client
60
30
 
61
31
  # yarn
62
32
  yarn add ani-client
33
+
34
+ # bun
35
+ bun add ani-client
63
36
  ```
64
37
 
65
38
  ## Quick start
@@ -80,516 +53,6 @@ const results = await client.searchMedia({
80
53
  perPage: 5,
81
54
  });
82
55
  console.log(results.results.map((m) => m.title.english));
83
-
84
- // Trending anime
85
- const trending = await client.getTrending(MediaType.ANIME);
86
- console.log(trending.results[0].title.romaji);
87
- ```
88
-
89
- <details>
90
- <summary>CommonJS</summary>
91
-
92
- ```js
93
- const { AniListClient } = require("ani-client");
94
-
95
- const client = new AniListClient();
96
- client.getMedia(1).then((anime) => console.log(anime.title.romaji));
97
- ```
98
-
99
- </details>
100
-
101
- ## Client options
102
-
103
- ```ts
104
- const client = new AniListClient({
105
- // AniList OAuth bearer token (optional)
106
- token: "your-token",
107
-
108
- // Custom API endpoint
109
- apiUrl: "https://graphql.anilist.co",
110
-
111
- // In-memory cache settings
112
- cache: {
113
- ttl: 86_400_000, // 24 hours (ms)
114
- maxSize: 500, // max entries (LRU eviction)
115
- enabled: true,
116
- },
117
-
118
- // Or bring your own adapter (takes precedence over `cache`)
119
- cacheAdapter: new RedisCache({ client: redisClient }),
120
-
121
- // Rate limiter
122
- rateLimit: {
123
- maxRequests: 85, // per window (AniList allows 90)
124
- windowMs: 60_000, // 1 minute
125
- maxRetries: 3, // retries on 429
126
- retryDelayMs: 2_000, // delay between retries
127
- timeoutMs: 30_000, // per-request timeout (0 = none)
128
- retryOnNetworkError: true, // retry ECONNRESET, ETIMEDOUT, etc.
129
- enabled: true,
130
- },
131
-
132
- // Event hooks
133
- hooks: {
134
- onRequest: (query, variables) => {},
135
- onResponse: (query, durationMs, fromCache) => {},
136
- onCacheHit: (key) => {},
137
- onRateLimit: (retryAfterMs) => {},
138
- onRetry: (attempt, reason, delayMs) => {},
139
- },
140
- });
141
- ```
142
-
143
- ## API reference
144
-
145
- ### Media
146
-
147
- | Method | Description |
148
- | --- | --- |
149
- | `getMedia(id, include?)` | Fetch a single anime / manga by ID with optional extra data |
150
- | `searchMedia(options?)` | Search & filter anime / manga |
151
- | `getTrending(type?, page?, perPage?)` | Currently trending entries |
152
- | `getMediaBySeason(options)` | Anime/manga for a given season & year |
153
- | `getRecommendations(mediaId, options?)` | User recommendations for a media |
154
-
155
- ```ts
156
- // Simple — same as before
157
- const anime = await client.getMedia(1);
158
-
159
- // With extra data
160
- const anime = await client.getMedia(1, {
161
- characters: { perPage: 25, sort: true },
162
- staff: true,
163
- relations: true,
164
- streamingEpisodes: true,
165
- externalLinks: true,
166
- stats: true,
167
- recommendations: { perPage: 5 },
168
- });
169
-
170
- const results = await client.searchMedia({
171
- query: "Naruto",
172
- type: MediaType.ANIME,
173
- format: MediaFormat.TV,
174
- genre: "Action",
175
- perPage: 10,
176
- });
177
- ```
178
-
179
- ### Include options
180
-
181
- The second parameter of `getMedia()` lets you opt-in to additional data. By default, only `relations` are included for backward compatibility.
182
-
183
- | Option | Type | Default | Description |
184
- | --- | --- | --- | --- |
185
- | `characters` | `boolean \| { perPage?, sort?, voiceActors? }` | — | Characters with their roles (MAIN, SUPPORTING, BACKGROUND). Set `voiceActors: true` to include VA data. |
186
- | `staff` | `boolean \| { perPage?, sort? }` | — | Staff members with their roles |
187
- | `relations` | `boolean` | `true` | Sequels, prequels, adaptations, etc. Set `false` to exclude |
188
- | `streamingEpisodes` | `boolean` | — | Streaming links (Crunchyroll, Funimation, etc.) |
189
- | `externalLinks` | `boolean` | — | External links (MAL, official site, etc.) |
190
- | `stats` | `boolean` | — | Score & status distribution |
191
- | `recommendations` | `boolean \| { perPage? }` | — | User recommendations |
192
-
193
- ```ts
194
- // Include characters sorted by role (25 per page by default)
195
- const anime = await client.getMedia(1, { characters: true });
196
- anime.characters?.edges.forEach((e) =>
197
- console.log(`${e.node.name.full} (${e.role})`)
198
- );
199
-
200
- // 50 characters, no sorting
201
- const anime = await client.getMedia(1, {
202
- characters: { perPage: 50, sort: false },
203
- });
204
-
205
- // Include voice actors alongside characters
206
- const anime = await client.getMedia(1, {
207
- characters: { voiceActors: true },
208
- });
209
- anime.characters?.edges.forEach((e) => {
210
- console.log(e.node.name.full);
211
- e.voiceActors?.forEach((va) =>
212
- console.log(` VA: ${va.name.full} (${va.languageV2})`)
213
- );
214
- });
215
-
216
- // Staff members
217
- const anime = await client.getMedia(1, { staff: true });
218
- anime.staff?.edges.forEach((e) =>
219
- console.log(`${e.node.name.full} — ${e.role}`)
220
- );
221
-
222
- // Everything at once
223
- const anime = await client.getMedia(1, {
224
- characters: { perPage: 50 },
225
- staff: { perPage: 25 },
226
- relations: true,
227
- streamingEpisodes: true,
228
- externalLinks: true,
229
- stats: true,
230
- recommendations: { perPage: 10 },
231
- });
232
-
233
- // Lightweight — exclude relations
234
- const anime = await client.getMedia(1, {
235
- characters: true,
236
- relations: false,
237
- });
238
- ```
239
-
240
- ### Characters
241
-
242
- | Method | Description |
243
- | --- | --- |
244
- | `getCharacter(id, include?)` | Fetch a character by ID, optionally with voice actors |
245
- | `searchCharacters(options?)` | Search characters by name, optionally with voice actors |
246
-
247
- ```ts
248
- const spike = await client.getCharacter(1);
249
- const results = await client.searchCharacters({ query: "Luffy", perPage: 5 });
250
-
251
- // With voice actors
252
- const spike = await client.getCharacter(1, { voiceActors: true });
253
- spike.media?.edges?.forEach((e) => {
254
- console.log(e.node.title.romaji);
255
- e.voiceActors?.forEach((va) =>
256
- console.log(` VA: ${va.name.full} (${va.languageV2})`)
257
- );
258
- });
259
-
260
- // Search with voice actors
261
- const result = await client.searchCharacters({ query: "Luffy", voiceActors: true });
262
- ```
263
-
264
- ### Staff
265
-
266
- | Method | Description |
267
- | --- | --- |
268
- | `getStaff(id, include?)` | Fetch a staff member by ID (optionally with media) |
269
- | `searchStaff(options?)` | Search for staff members |
270
-
271
- ```ts
272
- const staff = await client.getStaff(95001);
273
- const results = await client.searchStaff({ query: "Miyazaki" });
274
-
275
- // With media the staff member worked on
276
- const staffWithMedia = await client.getStaff(95001, { media: true });
277
- staffWithMedia.staffMedia?.nodes.forEach((m) => {
278
- console.log(m.title.romaji, m.format, m.averageScore);
279
- });
280
-
281
- // Customize the number of media returned
282
- const staffWith5Media = await client.getStaff(95001, { media: { perPage: 5 } });
283
- ```
284
-
285
- ### Users
286
-
287
- | Method | Description |
288
- | --- | --- |
289
- | `getUser(id)` | Fetch a user by ID |
290
- | `getUserByName(name)` | Fetch a user by username |
291
- | `getUserMediaList(options)` | Get a user's anime or manga list |
292
-
293
- ### Airing, Chapters & Planning
294
-
295
- | Method | Description |
296
- | --- | --- |
297
- | `getAiredEpisodes(options?)` | Recently aired anime episodes (last 24 h by default) |
298
- | `getAiredChapters(options?)` | Recently updated releasing manga |
299
- | `getPlanning(options?)` | Upcoming not-yet-released anime / manga |
300
-
301
- ```ts
302
- // Episodes aired in the last 7 days
303
- const week = await client.getAiredEpisodes({
304
- airingAtGreater: Math.floor(Date.now() / 1000) - 7 * 24 * 3600,
305
- perPage: 50,
306
- });
307
-
308
- // Most anticipated upcoming anime
309
- const upcoming = await client.getPlanning({
310
- type: MediaType.ANIME,
311
- perPage: 10,
312
- });
313
- ```
314
-
315
- ### Season charts
316
-
317
- ```ts
318
- import { MediaSeason } from "ani-client";
319
-
320
- const winter2026 = await client.getMediaBySeason({
321
- season: MediaSeason.WINTER,
322
- seasonYear: 2026,
323
- perPage: 25,
324
- });
325
- ```
326
-
327
- | Option | Type | Default | Description |
328
- | --- | --- | --- | --- |
329
- | `season` | `MediaSeason` | **(required)** | WINTER, SPRING, SUMMER, or FALL |
330
- | `seasonYear` | `number` | **(required)** | The year |
331
- | `type` | `MediaType` | `ANIME` | Filter by ANIME or MANGA |
332
- | `sort` | `MediaSort[]` | `["POPULARITY_DESC"]` | Sort order |
333
- | `page` | `number` | `1` | Page number |
334
- | `perPage` | `number` | `20` | Results per page (max 50) |
335
-
336
- ### User media lists
337
-
338
- ```ts
339
- import { MediaType, MediaListStatus } from "ani-client";
340
-
341
- const list = await client.getUserMediaList({
342
- userName: "AniList",
343
- type: MediaType.ANIME,
344
- status: MediaListStatus.COMPLETED,
345
- });
346
- list.results.forEach((entry) =>
347
- console.log(`${entry.media.title.romaji} — ${entry.score}/100`)
348
- );
349
- ```
350
-
351
- Provide either `userId` or `userName`. The `type` field is required.
352
-
353
- ### Recommendations
354
-
355
- ```ts
356
- const recs = await client.getRecommendations(1); // Cowboy Bebop
357
- recs.results.forEach((r) =>
358
- console.log(`${r.mediaRecommendation.title.romaji} (rating: ${r.rating})`)
359
- );
360
- ```
361
-
362
- ### Relations
363
-
364
- Relations (sequels, prequels, spin-offs, etc.) are included by default when using `getMedia()`. You can also explicitly request them via the `include` parameter, or exclude them with `relations: false`.
365
-
366
- ```ts
367
- const anime = await client.getMedia(1);
368
- anime.relations?.edges.forEach((edge) =>
369
- console.log(`${edge.relationType}: ${edge.node.title.romaji}`)
370
- );
371
-
372
- // Exclude relations for a lighter response
373
- const anime = await client.getMedia(1, { relations: false });
374
- ```
375
-
376
- Available types: `ADAPTATION`, `PREQUEL`, `SEQUEL`, `PARENT`, `SIDE_STORY`, `CHARACTER`, `SUMMARY`, `ALTERNATIVE`, `SPIN_OFF`, `OTHER`, `SOURCE`, `COMPILATION`, `CONTAINS`.
377
-
378
- ### Studios
379
-
380
- | Method | Description |
381
- | --- | --- |
382
- | `getStudio(id)` | Fetch a studio with its productions |
383
- | `searchStudios(options?)` | Search studios by name |
384
-
385
- ```ts
386
- const studio = await client.getStudio(44); // Bones
387
- const results = await client.searchStudios({ query: "MAPPA" });
388
- ```
389
-
390
- ### Genres & Tags
391
-
392
- ```ts
393
- const genres = await client.getGenres();
394
- // ["Action", "Adventure", "Comedy", ...]
395
-
396
- const tags = await client.getTags();
397
- // [{ id, name, description, category, isAdult }, ...]
398
- ```
399
-
400
- ### Batch queries
401
-
402
- Fetch multiple IDs in a single GraphQL request (up to 50 per call, auto-chunked).
403
-
404
- | Method | Description |
405
- | --- | --- |
406
- | `getMediaBatch(ids)` | Fetch multiple anime / manga |
407
- | `getCharacterBatch(ids)` | Fetch multiple characters |
408
- | `getStaffBatch(ids)` | Fetch multiple staff members |
409
-
410
- ```ts
411
- const [bebop, naruto, aot] = await client.getMediaBatch([1, 20, 16498]);
412
- ```
413
-
414
- ### Raw queries
415
-
416
- Execute any GraphQL query against the AniList API.
417
-
418
- ```ts
419
- const data = await client.raw<{ Media: { id: number; title: { romaji: string } } }>(
420
- "query { Media(id: 1) { id title { romaji } } }",
421
- );
422
- ```
423
-
424
- ### Auto-pagination
425
-
426
- `paginate()` returns an async iterator that fetches pages on demand.
427
-
428
- ```ts
429
- for await (const anime of client.paginate(
430
- (page) => client.searchMedia({ query: "Gundam", page, perPage: 10 }),
431
- 3, // max 3 pages
432
- )) {
433
- console.log(anime.title.romaji);
434
- }
435
- ```
436
-
437
- ## Caching
438
-
439
- ### Memory cache
440
-
441
- The default in-memory cache uses **LRU eviction** (24 h TTL, 500 entries max).
442
-
443
- ```ts
444
- const client = new AniListClient({
445
- cache: {
446
- ttl: 1000 * 60 * 60, // 1 hour
447
- maxSize: 200,
448
- enabled: true, // false to disable
449
- },
450
- });
451
- ```
452
-
453
- ### Redis cache
454
-
455
- For distributed or persistent caching, use the built-in Redis adapter. Compatible with [ioredis](https://github.com/redis/ioredis) and [node-redis](https://github.com/redis/node-redis) v4+.
456
-
457
- ```ts
458
- import Redis from "ioredis";
459
- import { AniListClient, RedisCache } from "ani-client";
460
-
461
- const client = new AniListClient({
462
- cacheAdapter: new RedisCache({
463
- client: new Redis(),
464
- prefix: "ani:", // key prefix (default)
465
- ttl: 86_400, // seconds (default: 24 h)
466
- }),
467
- });
468
- ```
469
-
470
- ### Custom adapter
471
-
472
- Implement the `CacheAdapter` interface to bring your own storage:
473
-
474
- ```ts
475
- import type { CacheAdapter } from "ani-client";
476
-
477
- class MyCache implements CacheAdapter {
478
- get<T>(key: string): T | undefined | Promise<T | undefined> { /* ... */ }
479
- set<T>(key: string, data: T): void | Promise<void> { /* ... */ }
480
- delete(key: string): boolean | Promise<boolean> { /* ... */ }
481
- clear(): void | Promise<void> { /* ... */ }
482
- get size(): number { return -1; } // return -1 if unknown
483
- keys(): string[] | Promise<string[]> { /* ... */ }
484
- // Optional — the client provides a fallback if omitted
485
- invalidate?(pattern: string | RegExp): number | Promise<number> { /* ... */ }
486
- }
487
- ```
488
-
489
- ### Cache invalidation
490
-
491
- ```ts
492
- // Clear everything
493
- await client.clearCache();
494
-
495
- // Remove entries matching a pattern
496
- const removed = await client.invalidateCache(/Media/);
497
- console.log(`Removed ${removed} entries`);
498
-
499
- // Current cache size
500
- console.log(client.cacheSize);
501
- ```
502
-
503
- ## Rate limiting
504
-
505
- The client respects AniList's rate limit (90 req/min) with a conservative default of **85 req/min**. On HTTP 429, it retries automatically with progressive backoff.
506
-
507
- ```ts
508
- const client = new AniListClient({
509
- rateLimit: {
510
- maxRequests: 60,
511
- windowMs: 60_000,
512
- maxRetries: 5,
513
- retryDelayMs: 3_000,
514
- timeoutMs: 30_000, // abort after 30 s
515
- retryOnNetworkError: true, // retry ECONNRESET, ETIMEDOUT, etc.
516
- enabled: true,
517
- },
518
- });
519
- ```
520
-
521
- ## Request deduplication
522
-
523
- When multiple callers request the same data at the same time, only one API call is made. All callers receive the same response.
524
-
525
- ```ts
526
- // Only 1 HTTP request is sent
527
- const [a, b] = await Promise.all([
528
- client.getMedia(1),
529
- client.getMedia(1),
530
- ]);
531
- ```
532
-
533
- ## Event hooks
534
-
535
- Monitor every request lifecycle event for logging, metrics, or debugging.
536
-
537
- ```ts
538
- const client = new AniListClient({
539
- hooks: {
540
- onRequest: (query, variables) => console.log("→", query.slice(0, 40)),
541
- onResponse: (query, durationMs, fromCache) => console.log(`← ${durationMs}ms (cache: ${fromCache})`),
542
- onCacheHit: (key) => console.log("Cache hit:", key.slice(0, 30)),
543
- onRateLimit: (retryAfterMs) => console.warn(`Rate limited, waiting ${retryAfterMs}ms`),
544
- onRetry: (attempt, reason, delayMs) => console.warn(`Retry #${attempt}: ${reason}`),
545
- },
546
- });
547
- ```
548
-
549
- ## Error handling
550
-
551
- All API errors throw an `AniListError` with `message`, `status` and `errors`:
552
-
553
- ```ts
554
- import { AniListError } from "ani-client";
555
-
556
- try {
557
- await client.getMedia(999999999);
558
- } catch (err) {
559
- if (err instanceof AniListError) {
560
- console.error(err.message); // "Not Found."
561
- console.error(err.status); // 404
562
- console.error(err.errors); // raw API error array
563
- }
564
- }
565
- ```
566
-
567
- ## Types
568
-
569
- All types and enums are exported:
570
-
571
- ```ts
572
- import type {
573
- Media, Character, Staff, User, VoiceActor,
574
- AiringSchedule, MediaListEntry, Recommendation, StudioDetail,
575
- MediaEdge, MediaConnection, MediaCharacterEdge, MediaCharacterConnection,
576
- CharacterMediaEdge, CharacterIncludeOptions,
577
- MediaStaffEdge, MediaStaffConnection, MediaIncludeOptions,
578
- StaffMediaNode, StaffIncludeOptions,
579
- StreamingEpisode, ExternalLink, MediaStats, MediaRecommendationNode,
580
- PageInfo, PagedResult,
581
- CacheAdapter, AniListHooks, AniListClientOptions,
582
- SearchMediaOptions, SearchCharacterOptions, SearchStaffOptions,
583
- SearchStudioOptions, GetAiringOptions, GetRecentChaptersOptions,
584
- GetPlanningOptions, GetSeasonOptions, GetUserMediaListOptions,
585
- GetRecommendationsOptions,
586
- } from "ani-client";
587
-
588
- import {
589
- MediaType, MediaFormat, MediaStatus, MediaSeason, MediaSort,
590
- CharacterSort, CharacterRole, AiringSort, RecommendationSort,
591
- MediaRelationType, MediaListStatus, MediaListSort,
592
- } from "ani-client";
593
56
  ```
594
57
 
595
58
  ## Contributing