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