myanimelist-wrapper
Version:
A comprehensive TypeScript wrapper for the Jikan API v4 (unofficial MyAnimeList API)
831 lines (636 loc) • 21.6 kB
Markdown
# MyAnimeList Wrapper
<div align="center">
[](https://www.npmjs.com/package/myanimelist-wrapper)
[](https://www.npmjs.com/package/myanimelist-wrapper)
[](https://opensource.org/licenses/MIT)
[](https://github.com/firrthecreator/myanimelist-wrapper/actions/workflows/ci.yml)
[](https://www.typescriptlang.org/)
[](https://nodejs.org/)
**A type-safe, feature-complete TypeScript wrapper for the Jikan API v4**
[Documentation](#documentation) • [Examples](#examples) • [API Reference](#api-reference) • [Contributing](CONTRIBUTING.md)
</div>
---
## Overview
A comprehensive, production-ready TypeScript wrapper for the **Jikan API v4** (unofficial MyAnimeList API). Built with type safety, performance, and developer experience in mind.
Provides seamless access to MyAnimeList data including anime, manga, characters, people, and more with full TypeScript support and robust error handling.
## ✨ Features
- **🎯 Type-Safe**: Full TypeScript support with comprehensive type definitions for all API responses
- **📦 Complete Coverage**: All Jikan API v4 endpoints implemented and tested
- **⚡ Performance Optimized**: Built-in caching, request batching, and connection pooling support
- **🛡️ Robust Error Handling**: Custom error classes with detailed error information and recovery suggestions
- **📚 Comprehensive Documentation**: Extensive JSDoc comments, examples, and API guides
- **🧪 Well Tested**: Full unit test coverage with vitest
- **🔧 Zero Dependencies**: Minimal footprint using native Node.js APIs
- **🎨 Clean API**: Intuitive, fluent API design for common use cases
- **♻️ Tree-Shakeable**: Modular architecture enables optimal bundle sizes
- **📊 Rate Limit Aware**: Built-in rate limit handling and request throttling
## 📦 Installation
### Using npm
```bash
npm install myanimelist-wrapper
```
### Using yarn
```bash
yarn add myanimelist-wrapper
```
### Using pnpm
```bash
pnpm add myanimelist-wrapper
```
### Requirements
- Node.js 16 or higher
- TypeScript 4.7 or higher (for TypeScript projects)
## 🚀 Quick Start
### Basic Usage
```typescript
import { JikanClient } from 'myanimelist-wrapper';
// Initialize the client
const jikan = new JikanClient();
// Fetch anime information
const getAnime = async () => {
try {
const response = await jikan.anime.getById(5114); // Fullmetal Alchemist: Brotherhood
console.log(response.data.title);
console.log(response.data.score);
} catch (error) {
console.error('Error fetching anime:', error);
}
};
// Search for anime
const searchAnime = async () => {
try {
const results = await jikan.anime.search({
q: 'attack on titan',
limit: 5,
type: 'tv'
});
results.data.forEach(anime => {
console.log(`${anime.title} (${anime.year})`);
});
} catch (error) {
console.error('Error searching anime:', error);
}
};
getAnime();
searchAnime();
```
## 📖 Documentation
### Table of Contents
1. **[Client Configuration](#client-configuration)** - Configure the JikanClient
2. **[API Endpoints](#-available-endpoints)** - All available endpoints
3. **[Examples](#-examples)** - Real-world usage examples
4. **[Error Handling](#-error-handling)** - Handle errors gracefully
5. **[Rate Limiting](#-rate-limiting)** - Respect API rate limits
6. **[Pagination](#-pagination)** - Navigate through results
7. **[Advanced Usage](#-advanced-usage)** - Advanced patterns and techniques
8. **[TypeScript Types](#-typescript-types)** - Type definitions
For complete API reference, see [API Documentation](docs/API.md).
### Client Configuration
```typescript
import { JikanClient } from 'myanimelist-wrapper';
// Default configuration
const jikan = new JikanClient();
// Custom configuration
const jikanCustom = new JikanClient({
// Base URL of the Jikan API
baseUrl: 'https://api.jikan.moe/v4',
// Request timeout in milliseconds
timeout: 30000,
// Custom headers
headers: {
'Accept': 'application/json',
'User-Agent': 'MyAnimeList-Wrapper/1.0'
},
// Enable/disable automatic retries
retryAttempts: 3,
retryDelay: 1000,
// Cache configuration
cacheEnabled: true,
cacheTTL: 3600000 // 1 hour
});
```
### Configuration Options
| Option | Type | Default | Description |
|--------|------|---------|-------------|
| `baseUrl` | string | `https://api.jikan.moe/v4` | The base URL for the Jikan API |
| `timeout` | number | `30000` | Request timeout in milliseconds |
| `headers` | object | `{}` | Custom HTTP headers |
| `retryAttempts` | number | `3` | Number of retry attempts on failure |
| `retryDelay` | number | `1000` | Delay between retries in milliseconds |
| `cacheEnabled` | boolean | `true` | Enable response caching |
| `cacheTTL` | number | `3600000` | Cache time-to-live in milliseconds |
### 🔌 Available Endpoints
The client provides access to all Jikan API v4 endpoints through these properties:
| Endpoint | Module | Methods |
|----------|--------|---------|
| **Anime** | `jikan.anime` | `getById()`, `search()`, `getCharacters()`, `getEpisodes()`, `getStatistics()`, `getForums()`, `getNews()`, `getRecommendations()`, `getRelations()` |
| **Manga** | `jikan.manga` | `getById()`, `search()`, `getCharacters()`, `getForums()`, `getNews()`, `getRelations()` |
| **Characters** | `jikan.characters` | `getById()`, `search()`, `getPictures()` |
| **People** | `jikan.people` | `getById()`, `search()`, `getPictures()` |
| **Clubs** | `jikan.clubs` | `getById()`, `search()`, `getRelations()`, `getMembers()` |
| **Seasons** | `jikan.seasons` | `getCurrent()`, `getSeason()`, `getUpcoming()` |
| **Schedules** | `jikan.schedules` | `getSchedule()` |
| **Top** | `jikan.top` | `getAnime()`, `getManga()`, `getCharacters()`, `getPeople()` |
| **Genres** | `jikan.genres` | `getAnimeGenres()`, `getMangaGenres()`, `getAnimeExplicitGenres()`, `getMangaExplicitGenres()` |
| **Producers** | `jikan.producers` | `getById()`, `search()` |
| **Magazines** | `jikan.magazines` | `getById()`, `search()` |
| **Users** | `jikan.users` | `getById()`, `search()`, `getFullProfile()`, `getHistory()` |
| **Reviews** | `jikan.reviews` | `getAnimeReviews()`, `getMangaReviews()` |
| **Recommendations** | `jikan.recommendations` | `getRecommendations()` |
| **Random** | `jikan.random` | `getAnime()`, `getManga()`, `getCharacter()`, `getPerson()`, `getUser()` |
### 📚 Examples
Complete examples are available in the [examples/](examples/) directory:
- [Basic Usage](examples/basic-usage.ts) - Simple API calls
- [Advanced Usage](examples/advanced-usage.ts) - Complex scenarios
- [Error Handling](examples/error-handling.ts) - Proper error management
- [Pagination](examples/pagination.ts) - Navigating through paginated results
- [Rate Limiting](examples/rate-limiting.ts) - Respecting API limits
#### Anime
**Get anime information:**
```typescript
// Get anime by ID with full details
const anime = await jikan.anime.getById(5114);
console.log(anime.data.title); // "Fullmetal Alchemist: Brotherhood"
console.log(anime.data.score); // 9.1
console.log(anime.data.episodes); // 64
console.log(anime.data.aired.from); // "2009-04-05"
```
**Search anime:**
```typescript
const results = await jikan.anime.search({
q: 'one piece',
type: 'tv',
status: 'airing',
rating: 'pg13',
score: 7,
limit: 25,
page: 1
});
results.data.forEach(anime => {
console.log(`${anime.title} - ${anime.score}`);
});
// Check pagination
if (results.pagination.has_next_page) {
const nextPage = await jikan.anime.search({
q: 'one piece',
page: 2
});
}
```
**Get anime characters and voice actors:**
```typescript
const characters = await jikan.anime.getCharacters(5114);
characters.data.forEach(char => {
console.log(`${char.character.name} (${char.role})`);
if (char.voice_actors.length > 0) {
console.log(` VA: ${char.voice_actors[0].person.name}`);
}
});
```
**Get anime episodes:**
```typescript
const episodes = await jikan.anime.getEpisodes(5114, 1); // Page 1
episodes.data.forEach(ep => {
console.log(`Episode ${ep.mal_id}: ${ep.title}`);
console.log(` Air date: ${ep.aired}`);
});
```
**Get anime statistics:**
```typescript
const stats = await jikan.anime.getStatistics(5114);
stats.data.forEach(stat => {
console.log(`${stat.title}: ${stat.count} users`);
});
```
#### Manga
**Get manga information:**
```typescript
const manga = await jikan.manga.getById(1);
console.log(manga.data.title); // "Berserk"
console.log(manga.data.type); // "Manga"
console.log(manga.data.chapters); // 364 (or null if ongoing)
```
**Search manga:**
```typescript
const results = await jikan.manga.search({
q: 'berserk',
type: 'manga',
status: 'publishing',
score: 8,
limit: 10
});
results.data.forEach(manga => {
console.log(`${manga.title} (${manga.chapters} chapters)`);
});
```
**Get manga characters:**
```typescript
const characters = await jikan.manga.getCharacters(1);
characters.data.forEach(char => {
console.log(`${char.character.name} (${char.role})`);
});
```
#### Characters
**Get character information:**
```typescript
const character = await jikan.characters.getById(1);
console.log(character.data.name); // "Monkey D. Luffy"
console.log(character.data.about); // Character biography
console.log(character.data.mal_id); // MyAnimeList ID
```
**Search characters:**
```typescript
const results = await jikan.characters.search({
q: 'luffy',
limit: 10,
page: 1
});
results.data.forEach(char => {
console.log(`${char.name} from ${char.anime[0]?.title}`);
});
```
**Get character pictures:**
```typescript
const pictures = await jikan.characters.getPictures(1);
pictures.data.forEach(pic => {
console.log(pic.jpg.image_url);
});
```
#### Seasons
**Get current season:**
```typescript
const currentSeason = await jikan.seasons.getCurrent();
currentSeason.data.forEach(anime => {
console.log(`${anime.title} (${anime.season})`);
});
```
**Get specific season:**
```typescript
const winter2024 = await jikan.seasons.getSeason(2024, 'winter');
winter2024.data.forEach(anime => {
console.log(`${anime.title} - Episodes: ${anime.episodes}`);
});
```
**Get upcoming season:**
```typescript
const upcoming = await jikan.seasons.getUpcoming();
upcoming.data.slice(0, 5).forEach(anime => {
console.log(`${anime.title} (${anime.aired.from})`);
});
```
#### Top
**Get top anime:**
```typescript
const topAnime = await jikan.top.getAnime({
filter: 'airing',
limit: 25,
page: 1
});
topAnime.data.forEach((anime, index) => {
console.log(`${index + 1}. ${anime.title} - ${anime.score}`);
});
```
**Get top manga:**
```typescript
const topManga = await jikan.top.getManga({
filter: 'publishing',
limit: 10
});
topManga.data.forEach((manga, index) => {
console.log(`${index + 1}. ${manga.title} - ${manga.score}`);
});
```
**Get top characters and people:**
```typescript
const topCharacters = await jikan.top.getCharacters({ limit: 10 });
const topPeople = await jikan.top.getPeople({ limit: 10 });
```
#### Random
**Get random content:**
```typescript
const randomAnime = await jikan.random.getAnime();
console.log(`Random anime: ${randomAnime.data.title}`);
const randomManga = await jikan.random.getManga();
console.log(`Random manga: ${randomManga.data.title}`);
const randomChar = await jikan.random.getCharacter();
console.log(`Random character: ${randomChar.data.name}`);
```
#### Users
**Get user profile:**
```typescript
const user = await jikan.users.getById('firrthecreator');
console.log(user.data.username);
console.log(user.data.gender);
console.log(user.data.joined_date);
```
**Get user full profile:**
```typescript
const fullProfile = await jikan.users.getFullProfile('firrthecreator');
console.log(fullProfile.data.statistics);
console.log(fullProfile.data.favorites);
```
**Get user history:**
```typescript
const history = await jikan.users.getHistory('firrthecreator', 'anime');
history.data.forEach(entry => {
console.log(`${entry.entry.title} - ${entry.increment} episodes`);
});
```
#### Other Endpoints
**Genres:**
```typescript
const animeGenres = await jikan.genres.getAnimeGenres();
animeGenres.data.forEach(genre => {
console.log(`${genre.name} - ${genre.count} anime`);
});
```
**Producers:**
```typescript
const producer = await jikan.producers.getById(1);
console.log(producer.data.titles[0].title);
const producerSearch = await jikan.producers.search({ query: 'sunrise' });
```
**Magazines:**
```typescript
const magazines = await jikan.magazines.search({ query: 'jump' });
```
**Reviews:**
```typescript
const animeReviews = await jikan.reviews.getAnimeReviews();
animeReviews.data.forEach(review => {
console.log(`${review.anime.title} - ${review.score}/10`);
console.log(review.review.substring(0, 100) + '...');
});
```
**Recommendations:**
```typescript
const recommendations = await jikan.recommendations.getRecommendations();
recommendations.data.forEach(rec => {
console.log(
`If you like ${rec.entry_1.title}, try ${rec.entry_2.title}`
);
});
```
### 🛡️ Error Handling
The client provides a custom `JikanError` class for comprehensive error handling:
```typescript
import { JikanError } from 'myanimelist-wrapper';
try {
const anime = await jikan.anime.getById(999999999);
} catch (error) {
if (error instanceof JikanError) {
// API-level error
console.error(`API Error [${error.status}]: ${error.message}`);
if (error.status === 404) {
console.log('Anime not found');
} else if (error.status === 429) {
console.log('Rate limited - retry after delay');
} else if (error.status === 500) {
console.log('Server error - try again later');
}
// Additional error details
if (error.data) {
console.error('Error details:', error.data);
}
} else if (error instanceof TypeError) {
// Network error
console.error('Network error:', error.message);
} else {
// Other errors
console.error('Unexpected error:', error);
}
}
```
**Common Error Codes:**
| Status | Meaning | Solution |
|--------|---------|----------|
| `400` | Bad Request | Check your query parameters |
| `404` | Not Found | Verify the ID/resource exists |
| `429` | Too Many Requests | Implement rate limiting (see below) |
| `500` | Server Error | Wait and retry |
### ⏱️ Rate Limiting
The Jikan API enforces rate limits. Respect them using these strategies:
**Simple Delay Between Requests:**
```typescript
// Delay helper
const wait = (ms: number) => new Promise(resolve => setTimeout(resolve, ms));
// Fetch with delays
async function fetchAnimeList(ids: number[]) {
for (const id of ids) {
try {
const anime = await jikan.anime.getById(id);
console.log(anime.data.title);
// Wait 400ms between requests to avoid rate limiting
await wait(400);
} catch (error) {
console.error(`Failed to fetch anime ${id}:`, error);
}
}
}
fetchAnimeList([1, 5, 10, 15, 20]);
```
**Queue-Based Rate Limiting:**
```typescript
class RequestQueue {
private queue: (() => Promise<any>)[] = [];
private processing = false;
private delayMs = 400;
async add<T>(fn: () => Promise<T>): Promise<T> {
return new Promise((resolve, reject) => {
this.queue.push(async () => {
try {
const result = await fn();
resolve(result);
} catch (error) {
reject(error);
}
});
this.process();
});
}
private async process() {
if (this.processing || this.queue.length === 0) return;
this.processing = true;
while (this.queue.length > 0) {
const fn = this.queue.shift();
if (fn) {
await fn();
await new Promise(r => setTimeout(r, this.delayMs));
}
}
this.processing = false;
}
}
// Usage
const queue = new RequestQueue();
const animes = [1, 5, 10, 15, 20];
const results = await Promise.all(
animes.map(id => queue.add(() => jikan.anime.getById(id)))
);
```
### 📄 Pagination
Navigate through paginated results:
```typescript
interface PaginationOptions {
limit?: number; // Items per page (default: 25, max: 25)
page?: number; // Page number (starts at 1)
}
// Get first page
const firstPage = await jikan.anime.search({
q: 'naruto',
limit: 10,
page: 1
});
console.log(`Results: ${firstPage.data.length}`);
console.log(`Current page: ${firstPage.pagination.current_page}`);
console.log(`Last page: ${firstPage.pagination.last_page}`);
console.log(`Has next: ${firstPage.pagination.has_next_page}`);
// Fetch all pages
async function fetchAllPages<T>(
getPage: (page: number) => Promise<{ data: T[], pagination: any }>
) {
const allResults: T[] = [];
let page = 1;
let hasMore = true;
while (hasMore) {
const response = await getPage(page);
allResults.push(...response.data);
hasMore = response.pagination.has_next_page;
page++;
// Rate limiting
await new Promise(r => setTimeout(r, 400));
}
return allResults;
}
// Usage
const allAnimes = await fetchAllPages(
page => jikan.anime.search({ q: 'dragon', page })
);
```
### 🔧 Advanced Usage
**Parallel Requests with Rate Limiting:**
```typescript
async function batchFetch(ids: number[], batchSize = 5) {
const results = [];
for (let i = 0; i < ids.length; i += batchSize) {
const batch = ids.slice(i, i + batchSize);
// Fetch batch in parallel
const batchResults = await Promise.all(
batch.map(id => jikan.anime.getById(id))
);
results.push(...batchResults);
// Delay between batches
if (i + batchSize < ids.length) {
await new Promise(r => setTimeout(r, 1000));
}
}
return results;
}
const animes = await batchFetch([1, 5, 10, 15, 20, 25], 3);
```
**Caching Layer:**
```typescript
class CachedClient {
private cache = new Map<string, { data: any, expiry: number }>();
private cacheTTL = 3600000; // 1 hour
async get<T>(key: string, fn: () => Promise<T>): Promise<T> {
const cached = this.cache.get(key);
if (cached && cached.expiry > Date.now()) {
return cached.data;
}
const data = await fn();
this.cache.set(key, { data, expiry: Date.now() + this.cacheTTL });
return data;
}
clear() {
this.cache.clear();
}
}
// Usage
const client = new CachedClient();
const anime = await client.get('anime:5114', () =>
jikan.anime.getById(5114)
);
```
### 📝 TypeScript Types
Full type definitions are available for all responses:
```typescript
import type {
Anime,
Manga,
Character,
Person,
AnimeSearchQuery,
MangaSearchQuery,
ApiResponse,
JikanError
} from 'myanimelist-wrapper';
// Type-safe API calls
async function exampleUsage() {
const response: ApiResponse<Anime> = await jikan.anime.getById(5114);
const anime: Anime = response.data;
// TypeScript knows about all properties
console.log(anime.episodes);
console.log(anime.aired);
console.log(anime.score);
// Search with type-safe parameters
const query: AnimeSearchQuery = {
q: 'naruto',
type: 'tv',
status: 'airing',
score: 8,
limit: 10
};
const searchResults: ApiResponse<Anime[]> =
await jikan.anime.search(query);
}
```
## 🤝 Contributing
We welcome contributions! Please see [CONTRIBUTING.md](CONTRIBUTING.md) for detailed guidelines.
### Development Setup
```bash
# Clone the repository
git clone https://github.com/firrthecreator/myanimelist-wrapper.git
cd myanimelist-wrapper
# Install dependencies
npm install
# Build the project
npm run build
# Run tests
npm test
# Run tests in watch mode
npm run test:watch
# Generate coverage report
npm run test:coverage
# Format code
npm run format
# Lint code
npm run lint
```
### Project Structure
```
├── src/
│ ├── client.ts # Main client class
│ ├── index.ts # Package exports
│ ├── endpoints/ # API endpoint implementations
│ └── types/ # TypeScript type definitions
├── tests/ # Test files
├── docs/ # Documentation
├── examples/ # Usage examples
└── package.json # Dependencies and scripts
```
## 📄 License
MIT © 2026 [Firr, The Creator](https://github.com/firrthecreator)
## 🙏 Acknowledgments
- [Jikan API](https://jikan.moe/) - The wonderful unofficial MyAnimeList API
- [MyAnimeList](https://myanimelist.net/) - The source of all anime/manga data
- Contributors and maintainers of this project
## 📞 Support
- **Issues**: [GitHub Issues](https://github.com/firrthecreator/myanimelist-wrapper/issues)
- **Discussions**: [GitHub Discussions](https://github.com/firrthecreator/myanimelist-wrapper/discussions)
- **Documentation**: [Complete API Documentation](docs/API.md)
---
<div align="center">
**Made with ❤️ by [Firr, The Creator](https://github.com/firrthecreator)**
[⬆ back to top](#myanimelist-wrapper)
</div>