anilist-wrapper
v1.0.1
Published
A comprehensive TypeScript-based AniList API wrapper utilizing GraphQL
Maintainers
Readme
AniList Wrapper
A production-ready TypeScript/JavaScript wrapper for the AniList GraphQL API with full type support, rate limiting, and modular architecture.
Installation • Documentation • Examples • Contributing
Overview
AniList Wrapper is a modern, fully-typed TypeScript library for seamless interaction with the AniList GraphQL API. Built with developer experience in mind, it provides an intuitive API, comprehensive error handling, and efficient rate limiting out of the box.
Key Features
- 🎯 Type-Safe: Full TypeScript support with comprehensive type definitions for all API responses
- ⚡ Performance: Built-in rate limiting to respect API quotas and optimize request handling
- 🧩 Modular: Clean separation of concerns with dedicated modules for anime, manga, characters, staff, and users
- 📦 GraphQL: Complete GraphQL query execution with custom query support
- 🔐 Authenticated Requests: OAuth 2.0 token support for user-specific operations
- 🧪 Well-Tested: Comprehensive test suite using Vitest
- 📚 Documented: Extensive documentation and examples for all features
- 🚀 Tree-Shakeable: Optimized for modern bundlers with ES modules support
Table of Contents
- Installation
- Quick Start
- Authentication
- Modules
- API Reference
- Rate Limiting
- Examples
- Error Handling
- Best Practices
- Troubleshooting
- Contributing
- License
Installation
Via npm
npm install anilist-wrapperVia yarn
yarn add anilist-wrapperVia pnpm
pnpm add anilist-wrapperRequirements
- Node.js 16 or higher
- TypeScript 4.5 or higher (for TypeScript projects)
Quick Start
Basic Usage
Get started with just a few lines of code:
import { AniListClient } from 'anilist-wrapper';
const client = new AniListClient();
// Fetch anime by ID
async function getAnimeInfo() {
try {
const anime = await client.anime.getById(1);
console.log(`Title: ${anime.title.english || anime.title.romaji}`);
console.log(`Episodes: ${anime.episodes}`);
console.log(`Status: ${anime.status}`);
} catch (error) {
console.error('Failed to fetch anime:', error);
}
}
getAnimeInfo();Search Anime
// Search for anime by name
async function searchAnime() {
const results = await client.anime.search({
search: 'Attack on Titan',
perPage: 5
});
console.log(`Found ${results.pageInfo.total} results`);
results.data.forEach(anime => {
console.log(`- ${anime.title.english || anime.title.romaji} (${anime.seasonYear})`);
});
}
searchAnime();Modules
The wrapper is organized into modules for different types of content:
| Module | Purpose | Example |
|--------|---------|---------|
| anime | Anime queries and operations | Get anime info, search, trending |
| manga | Manga queries and operations | Get manga info, search, trending |
| character | Character queries and operations | Get character info, search |
| staff | Staff queries and operations | Get staff info, search |
| user | User queries and operations | Get user profiles, authenticate |
| search | Cross-type search functionality | Search all content types at once |
Authentication
Get Your OAuth Token
To use authenticated features, you'll need an AniList OAuth token:
- Visit AniList Developer Settings
- Create a new API client
- Copy your access token
Using Authentication
import { AniListClient } from 'anilist-wrapper';
// Method 1: Pass token during initialization
const client = new AniListClient({
token: 'your_oauth_token_here'
});
// Method 2: Set token later
const client = new AniListClient();
client.setToken('your_oauth_token_here');
// Now you can access authenticated endpoints
async function getCurrentUser() {
const user = await client.user.getCurrentUser();
console.log(`Welcome, ${user.name}!`);
console.log(`Level: ${user.statistics.manga.statuses}`);
}
getCurrentUser();Authenticated Operations
// Get user's anime list
async function getUserAnimeList() {
const user = await client.user.getCurrentUser();
// Access user-specific data
}
// Get user by ID
async function getUserData() {
const user = await client.user.getById(12345);
console.log(`User: ${user.name}`);
}
// Get user by username
async function searchUser() {
const user = await client.user.getByName('username');
console.log(`ID: ${user.id}`);
}Examples
Get Trending Anime
/**
* Fetch currently trending anime
* Results are ordered by trending score
*/
async function getTrendingAnime() {
try {
const trending = await client.anime.getTrending({
perPage: 10,
page: 1
});
console.log('📈 Trending Anime:');
trending.data.forEach((anime, index) => {
console.log(`${index + 1}. ${anime.title.english || anime.title.romaji}`);
console.log(` Status: ${anime.status}`);
console.log(` Score: ${anime.meanScore}/100\n`);
});
} catch (error) {
console.error('Failed to fetch trending anime:', error);
}
}Search for Characters
/**
* Search for characters by name
* Returns matching characters with their roles and media appearances
*/
async function findCharacters() {
const results = await client.character.search({
search: 'Monkey D. Luffy',
perPage: 5
});
console.log(`Found ${results.pageInfo.total} characters:`);
results.data.forEach(character => {
const fullName = character.name.full;
const nativeName = character.name.native || 'N/A';
console.log(`- ${fullName} (${nativeName})`);
});
}
findCharacters();Get Manga by ID
/**
* Fetch detailed manga information
* Includes chapters, volumes, and publication status
*/
async function getMangaDetails() {
const manga = await client.manga.getById(30002); // Fullmetal Alchemist
console.log(`Title: ${manga.title.english || manga.title.romaji}`);
console.log(`Chapters: ${manga.chapters || 'Ongoing'}`);
console.log(`Volumes: ${manga.volumes || 'Ongoing'}`);
console.log(`Status: ${manga.status}`);
console.log(`Score: ${manga.meanScore}/100`);
}
getMangaDetails();Search Multiple Types (All-in-One)
/**
* Search across all content types at once
* Returns anime, manga, characters, and staff matches
*/
async function searchAll() {
const results = await client.search.searchAll('Naruto', {
perPage: 3
});
if (results.anime?.length) {
console.log('📺 Anime:');
results.anime.forEach(a => console.log(` - ${a.title.romaji}`));
}
if (results.manga?.length) {
console.log('📚 Manga:');
results.manga.forEach(m => console.log(` - ${m.title.romaji}`));
}
if (results.characters?.length) {
console.log('👤 Characters:');
results.characters.forEach(c => console.log(` - ${c.name.full}`));
}
}
searchAll();Custom GraphQL Query
/**
* Execute custom GraphQL queries for advanced use cases
* Allows full control over the query structure and returned fields
*/
const customQuery = `
query {
GenreCollection
MediaTagCollection(status: OFFICIAL)
}
`;
async function getCustomData() {
const result = await client.rawQuery(customQuery);
console.log('Available genres:', result.GenreCollection);
console.log('Media tags:', result.MediaTagCollection);
}
getCustomData();Rate Limiting
AniList enforces rate limits on API requests. This wrapper includes built-in rate limiting to help you manage requests efficiently.
Understanding Rate Limits
- Limit: 90 requests per minute per IP
- Window: 1 minute rolling window
- Rate Limiter: Built into the client automatically
Using the Rate Limiter
import { RateLimiter } from 'anilist-wrapper';
/**
* Create a rate limiter for managing concurrent requests
* Useful when making many requests in parallel
*
* @param requestsPerMinute - Maximum requests per minute (default: 60)
*/
const rateLimiter = new RateLimiter(60);
// Queue requests through the rate limiter
async function fetchMultipleAnime() {
const animeIds = [1, 5, 6, 7, 8]; // Attack on Titan, Cowboy Bebop, etc.
const promises = animeIds.map(id =>
rateLimiter.add(() => client.anime.getById(id))
);
const animes = await Promise.all(promises);
console.log('Fetched all anime with rate limiting applied');
}
fetchMultipleAnime();Batch Operations
/**
* Best practice: Batch your requests to minimize API calls
* Use the rate limiter for heavy workloads
*/
async function batchSearch() {
const queries = ['One Piece', 'Naruto', 'Bleach', 'My Hero Academia'];
const results = await Promise.all(
queries.map(query =>
rateLimiter.add(() =>
client.anime.search({ search: query, perPage: 5 })
)
)
);
results.forEach((result, index) => {
console.log(`${queries[index]}: ${result.pageInfo.total} matches`);
});
}
batchSearch();API Reference
AniListClient
The main client for interacting with the AniList API.
/**
* Initialize the AniList client
* @param options - Configuration options
* @param options.token - OAuth token for authenticated requests (optional)
* @param options.baseUrl - GraphQL endpoint URL (optional, defaults to official API)
*/
const client = new AniListClient({
token: 'YOUR_OAUTH_TOKEN',
baseUrl: 'https://graphql.anilist.co'
});Client Methods
| Method | Description | Returns |
|--------|-------------|---------|
| setToken(token: string) | Set or update the OAuth token | void |
| rawQuery<T>(query: string, variables?) | Execute raw GraphQL queries with full control | Promise<T> |
AnimeModule
Comprehensive anime queries and operations.
Methods
| Method | Description | Parameters |
|--------|-------------|-----------|
| getById(id: number) | Get anime by ID | id: number |
| search(options, pagination?) | Search for anime | AnimeFilterOptions, PaginationOptions? |
| getTrending(pagination?) | Get trending anime | PaginationOptions? |
| getPopular(pagination?) | Get popular anime | PaginationOptions? |
| getUpcoming(pagination?) | Get upcoming anime | PaginationOptions? |
| getAiring(pagination?) | Get currently airing anime | PaginationOptions? |
// Example: Get anime by ID
const anime = await client.anime.getById(1);
console.log(anime.title.english); // "Attack on Titan"
// Example: Search with filters
const results = await client.anime.search({
search: 'Death Note',
status: 'FINISHED',
sort: ['SCORE_DESC'],
perPage: 10,
page: 1
});MangaModule
Complete manga queries and operations.
Methods
| Method | Description | Parameters |
|--------|-------------|-----------|
| getById(id: number) | Get manga by ID | id: number |
| search(options, pagination?) | Search for manga | MangaFilterOptions, PaginationOptions? |
| getTrending(pagination?) | Get trending manga | PaginationOptions? |
| getPopular(pagination?) | Get popular manga | PaginationOptions? |
| getUpcoming(pagination?) | Get upcoming manga | PaginationOptions? |
| getReleasing(pagination?) | Get currently releasing manga | PaginationOptions? |
// Example: Get manga by ID
const manga = await client.manga.getById(30002);
console.log(manga.title.english); // "Fullmetal Alchemist"
// Example: Find trending manga
const trending = await client.manga.getTrending({
perPage: 5,
page: 1
});CharacterModule
Character-related queries and operations.
Methods
| Method | Description | Parameters |
|--------|-------------|-----------|
| getById(id: number) | Get character by ID | id: number |
| search(options, pagination?) | Search for characters | CharacterFilterOptions, PaginationOptions? |
| getPopular(pagination?) | Get popular characters | PaginationOptions? |
| getByMedia(mediaId, pagination?) | Get characters from anime/manga | mediaId: number, PaginationOptions? |
// Example: Search for a character
const characters = await client.character.search({
search: 'Luffy',
perPage: 5
});
// Example: Get characters from a specific anime
const characters = await client.character.getByMedia(1); // Characters from Attack on TitanUserModule
User-related queries and operations.
Methods
| Method | Description | Parameters | Requires Auth |
|--------|-------------|-----------|---------------|
| getById(id: number) | Get user by ID | id: number | No |
| getByName(name: string) | Get user by username | name: string | No |
| search(options, pagination?) | Search for users | UserFilterOptions, PaginationOptions? | No |
| getCurrentUser() | Get authenticated user info | None | ✅ Yes |
// Example: Get user by name
const user = await client.user.getByName('username');
console.log(user.about); // User bio
// Example: Get current authenticated user
const currentUser = await client.user.getCurrentUser();
console.log(`Welcome, ${currentUser.name}`);StaffModule
Staff-related queries and operations.
Methods
| Method | Description | Parameters |
|--------|-------------|-----------|
| getById(id: number) | Get staff by ID | id: number |
| search(options, pagination?) | Search for staff | StaffFilterOptions, PaginationOptions? |
| getPopular(pagination?) | Get popular staff | PaginationOptions? |
| getByMedia(mediaId, pagination?) | Get staff from anime/manga | mediaId: number, PaginationOptions? |
// Example: Search for voice actors
const staff = await client.staff.search({
search: 'Yuki Kaji',
perPage: 5
});
// Example: Get staff from a specific anime
const staffList = await client.staff.getByMedia(1); // Staff of Attack on TitanSearchModule
Cross-type search functionality.
Methods
| Method | Description | Parameters |
|--------|-------------|-----------|
| searchAll(query: string, pagination?) | Search all content types | query: string, PaginationOptions? |
// Example: Search across all types
const results = await client.search.searchAll('Fate', {
perPage: 5,
page: 1
});
console.log(results.anime); // Anime matches
console.log(results.manga); // Manga matches
console.log(results.characters); // Character matches
console.log(results.staff); // Staff matchesError Handling
The wrapper provides structured error handling for API operations:
import { AniListClient } from 'anilist-wrapper';
const client = new AniListClient();
async function safeApiCall() {
try {
const anime = await client.anime.getById(1);
console.log('Success:', anime.title);
} catch (error) {
if (error instanceof Error) {
console.error('API Error:', error.message);
// Handle specific error types
if (error.message.includes('404')) {
console.error('Anime not found');
} else if (error.message.includes('rate')) {
console.error('Rate limit exceeded, please wait');
}
}
}
}
safeApiCall();Best Practices for Error Handling
/**
* Wrapper function for safe API calls with retry logic
*/
async function withRetry<T>(
fn: () => Promise<T>,
maxRetries = 3
): Promise<T> {
for (let i = 0; i < maxRetries; i++) {
try {
return await fn();
} catch (error) {
if (i === maxRetries - 1) throw error;
// Exponential backoff
const delay = Math.pow(2, i) * 1000;
await new Promise(resolve => setTimeout(resolve, delay));
}
}
throw new Error('Max retries exceeded');
}
// Usage
const anime = await withRetry(() => client.anime.getById(1));Best Practices
1. Pagination
Always use pagination for search results to minimize API calls:
async function getAllResults(search: string) {
const allData = [];
let page = 1;
let hasNextPage = true;
while (hasNextPage) {
const results = await client.anime.search({
search,
perPage: 50, // Maximum allowed
page
});
allData.push(...results.data);
hasNextPage = results.pageInfo.hasNextPage;
page++;
}
return allData;
}2. Caching
Implement caching to reduce redundant API calls:
const cache = new Map();
async function getCachedAnime(id: number) {
if (cache.has(id)) {
return cache.get(id);
}
const anime = await client.anime.getById(id);
cache.set(id, anime);
return anime;
}3. Parallel Requests
Use the rate limiter for parallel requests:
const rateLimiter = new RateLimiter(60);
const animes = await Promise.all([
rateLimiter.add(() => client.anime.getById(1)),
rateLimiter.add(() => client.anime.getById(2)),
rateLimiter.add(() => client.anime.getById(3))
]);4. Environment Variables
Store tokens securely:
const client = new AniListClient({
token: process.env.ANILIST_TOKEN
});Troubleshooting
Common Issues
401 Unauthorized
Problem: Authentication token is invalid or expired.
Solution:
// Refresh your token from AniList developer settings
client.setToken('new_valid_token');429 Rate Limited
Problem: Too many requests made too quickly.
Solution:
// Use the built-in rate limiter
const limiter = new RateLimiter(60);
const result = await limiter.add(() => client.anime.search(...));Network Timeout
Problem: Request took too long to complete.
Solution:
// Implement retry logic
async function withTimeout<T>(
fn: () => Promise<T>,
ms = 10000
): Promise<T> {
return Promise.race([
fn(),
new Promise<T>((_, reject) =>
setTimeout(() => reject(new Error('Timeout')), ms)
)
]);
}Empty Results
Problem: Search returned no results.
Solution:
const results = await client.anime.search({
search: 'query',
perPage: 25 // Try with fewer results first
});
if (results.pageInfo.total === 0) {
console.log('No results found');
}Contributing
We welcome contributions from the community! Whether you're fixing bugs, adding features, or improving documentation, your help makes this project better.
Getting Started
- Fork the repository
- Clone your fork:
git clone https://github.com/firrthecreator/anilist-wrapper.git cd anilist-wrapper - Install dependencies:
npm install - Create a feature branch:
git checkout -b feature/amazing-feature
Development Workflow
# Build the project
npm run build
# Run tests
npm test
# Watch mode for development
npm run test:watch
# Check code coverage
npm run test:coverage
# Lint code
npm run lintSubmitting Changes
- Make your changes and commit them:
git commit -m 'Add some amazing feature' - Push to your branch:
git push origin feature/amazing-feature - Create a Pull Request with a clear description of your changes
Code Standards
- Use TypeScript for all code
- Follow existing code style and conventions
- Write tests for new features
- Update documentation as needed
- Ensure all tests pass before submitting PR
- Write clear, descriptive commit messages
Testing Requirements
- All new features must include tests
- Maintain or improve code coverage
- Tests should be in the
tests/directory - Use Vitest as the testing framework
License
This project is licensed under the MIT License - see the LICENSE file for details.
Support
- 📖 Documentation: Check the examples/ folder for usage patterns
- 🐛 Issues: Found a bug? Report it here
- 💬 Discussions: Have questions? Start a discussion
Made with ❤️ by Firr, The Creator.
