Skip to main content

Overview

AniDojo uses the Jikan API to fetch anime data from MyAnimeList. Jikan is a free, unofficial REST API that provides comprehensive anime, manga, and character information from the largest anime database.

Why Jikan API?

Free & No API Key

No registration or API key required. Start using immediately.

Comprehensive Data

50,000+ anime with detailed information from MyAnimeList.

Real-Time Updates

Live data synced with MyAnimeList database.

Rich Metadata

Genres, studios, ratings, episodes, and more.

No Configuration Required

Unlike most APIs, Jikan requires no setup or credentials. The integration is ready to use out of the box!
No environment variables or API keys needed. Just start making requests!

Rate Limiting

Jikan has rate limits to ensure fair usage:
  • 3 requests per second
  • 60 requests per minute
AniDojo’s API client handles this automatically with built-in rate limiting and request queuing.

API Client Implementation

The API client is located in src/lib/animeApi.ts and includes:
Requests are automatically queued and throttled:
class APIRateLimiter {
  private minInterval = 350; // 350ms = ~3 requests/second
  private queue: Array<Request> = [];
  
  async call<T>(url: string): Promise<T> {
    // Queues request and processes with rate limiting
  }
}
Responses are cached for 5 minutes to reduce API calls:
private cache = new Map<string, { data: any; timestamp: number }>();
private cacheExpiry = 5 * 60 * 1000; // 5 minutes
Failed requests are automatically retried with exponential backoff:
private async fetchWithRetry(
  url: string,
  retries = 3,
  backoff = 1000
): Promise<Response> {
  // Retry logic with exponential backoff
}

Available API Functions

All API functions are exported from src/lib/animeApi.ts:

Search Anime

Search for anime by title:
import { searchAnime } from '@/lib/animeApi';

const results = await searchAnime('Naruto', 1, 20);
// Returns: { data: JikanAnime[], pagination: {...} }
Parameters:
  • query - Search term
  • page - Page number (default: 1)
  • limit - Results per page (default: 20, max: 25)

Get Top Anime

Fetch the highest-rated anime:
import { getTopAnime } from '@/lib/animeApi';

const topAnime = await getTopAnime(1, 20);
// Returns top-rated anime from MyAnimeList
Optional Filters:
// Filter by type
const topTV = await getTopAnime(1, 20, 'tv');
const topMovies = await getTopAnime(1, 20, 'movie');

Get Anime by ID

Fetch detailed information for a specific anime:
import { getAnimeById } from '@/lib/animeApi';

const anime = await getAnimeById(1);
// Returns full anime details

Get Seasonal Anime

Fetch anime from a specific season:
import { getSeasonalAnime } from '@/lib/animeApi';

const winterAnime = await getSeasonalAnime(2024, 'winter');
const springAnime = await getSeasonalAnime(2024, 'spring');
Seasons: winter, spring, summer, fall

Get Current Season

Fetch anime airing this season:
import { getCurrentSeasonAnime } from '@/lib/animeApi';

const currentSeason = await getCurrentSeasonAnime(20);

Jikan API Response Format

All responses follow this structure:
interface JikanSearchResponse {
  data: JikanAnime[];
  pagination: {
    last_visible_page: number;
    has_next_page: boolean;
    current_page: number;
    items: {
      count: number;
      total: number;
      per_page: number;
    };
  };
}

Anime Object Structure

interface JikanAnime {
  mal_id: number;                   // MyAnimeList ID
  title: string;                    // Romaji title
  title_english?: string;           // English title
  title_japanese?: string;          // Japanese title
  images: {                         // Cover art images
    jpg: {
      image_url: string;            // Medium size
      small_image_url: string;
      large_image_url: string;
    };
    webp: { /* same structure */ };
  };
  type: string;                     // TV, Movie, OVA, etc.
  episodes: number;                 // Total episodes
  status: string;                   // Airing status
  aired: {
    from: string;                   // Start date
    to: string;                     // End date
  };
  score: number;                    // MyAnimeList score (1-10)
  rank: number;                     // Ranking position
  popularity: number;               // Popularity rank
  synopsis: string;                 // Plot description
  genres: Array<{                   // Genre tags
    mal_id: number;
    name: string;
  }>;
  studios: Array<{                  // Animation studios
    mal_id: number;
    name: string;
  }>;
  // ... and more!
}

Converting to AniDojo Format

Convert Jikan data to AniDojo’s internal format:
import { convertJikanToAnime } from '@/lib/animeApi';
import type { JikanAnime } from '@/lib/animeApi';

const jikanAnime: JikanAnime = { /* ... */ };
const anime = convertJikanToAnime(jikanAnime);

// Returns:
{
  id: 'mal-1',
  title: 'Cowboy Bebop',
  synopsis: '...',
  genres: ['Action', 'Adventure', 'Sci-Fi'],
  studio: 'Sunrise',
  year: 1998,
  episodes: 26,
  status: 'COMPLETED',
  coverArt: 'https://cdn.myanimelist.net/...',
  staff: { /* ... */ }
}

React Hooks for Anime Data

AniDojo provides custom React hooks in src/hooks/useAnime.ts:

useAnimeSearch Hook

import { useAnimeSearch } from '@/hooks/useAnime';

function SearchComponent() {
  const { data, loading, error, hasNextPage } = useAnimeSearch('Naruto', 1);
  
  if (loading) return <div>Loading...</div>;
  if (error) return <div>Error: {error.message}</div>;
  
  return (
    <div>
      {data.map(anime => (
        <AnimeCard key={anime.mal_id} anime={anime} />
      ))}
    </div>
  );
}

useTopAnime Hook

import { useTopAnime } from '@/hooks/useAnime';

function TopAnimeComponent() {
  const { data, loading, error } = useTopAnime(1, 20);
  
  return (
    <div>
      {data.map(anime => (
        <AnimeCard key={anime.mal_id} anime={anime} />
      ))}
    </div>
  );
}

Example: Search Page

Here’s how the search page uses the API:
app/search/page.tsx
'use client';

import { useState } from 'react';
import { useAnimeSearch } from '@/hooks/useAnime';
import { AnimeSearch } from '@/components/AnimeSearch';

export default function SearchPage() {
  const [query, setQuery] = useState('');
  const [page, setPage] = useState(1);
  
  const { data, loading, error, hasNextPage } = useAnimeSearch(query, page);
  
  return (
    <div>
      <AnimeSearch 
        onSearch={setQuery}
        results={data}
        loading={loading}
        error={error}
      />
      
      {/* Pagination */}
      <div>
        <button 
          onClick={() => setPage(p => Math.max(1, p - 1))}
          disabled={page === 1}
        >
          Previous
        </button>
        
        <span>Page {page}</span>
        
        <button 
          onClick={() => setPage(p => p + 1)}
          disabled={!hasNextPage}
        >
          Next
        </button>
      </div>
    </div>
  );
}

Caching Strategies

Built-in Cache

The API client includes automatic caching:
// First call - fetches from API
const anime1 = await searchAnime('Naruto');

// Second call within 5 minutes - uses cache
const anime2 = await searchAnime('Naruto');

Clear Cache

Manually clear the cache when needed:
import { clearAPICache } from '@/lib/animeApi';

// Clear all cached responses
clearAPICache();

React Query (Optional)

For more advanced caching, consider adding React Query:
npm install @tanstack/react-query
import { useQuery } from '@tanstack/react-query';
import { searchAnime } from '@/lib/animeApi';

function useAnimeSearch(query: string) {
  return useQuery({
    queryKey: ['anime', 'search', query],
    queryFn: () => searchAnime(query),
    staleTime: 5 * 60 * 1000, // 5 minutes
  });
}

Error Handling

The API client handles errors gracefully:
try {
  const anime = await searchAnime('Naruto');
} catch (error) {
  if (error.message.includes('429')) {
    // Rate limited - already handled with retries
    console.error('API rate limited');
  } else if (error.message.includes('network')) {
    // Network error
    console.error('Network error');
  } else {
    // Other error
    console.error('API error:', error);
  }
}

Testing the API

Test your API integration:
1

Test search endpoint

curl "https://api.jikan.moe/v4/anime?q=naruto&limit=5"
2

Test in your app

Navigate to /search in your app and search for “Naruto”
3

Check rate limiting

Make multiple rapid requests and verify they’re queued properly
4

Test caching

Search for the same anime twice and check console for cache hit

Alternative Anime APIs

If you want to use a different data source:
GraphQL API with more detailed data
  • Pros: GraphQL, detailed character info, user data
  • Cons: More complex queries, requires different client
  • Website: anilist.co/graphiql
REST API with social features
  • Pros: User profiles, follows, social features
  • Cons: Smaller database than MAL
  • Website: kitsu.docs.apiary.io
For movies and some anime

Best Practices

API Usage Best Practices:
  • ✅ Respect rate limits (handled automatically)
  • ✅ Cache responses when possible
  • ✅ Handle errors gracefully
  • ✅ Show loading states to users
  • ✅ Implement pagination for large result sets
  • ✅ Don’t hammer the API - be a good citizen

Troubleshooting

API returns 429 (Rate Limited)

  • The client automatically retries with backoff
  • If persistent, increase minInterval in animeApi.ts
  • Consider adding more aggressive caching

Search returns empty results

  • Verify the anime exists on MyAnimeList
  • Try different search terms (English vs Japanese)
  • Check API status: status.jikan.moe

Images not loading

  • MyAnimeList CDN may block some requests
  • Consider proxying images through your own CDN
  • Check CORS headers in browser console

Slow API responses

  • First request may be slower (cache warming)
  • Consider prefetching popular anime
  • Implement skeleton loading states

Next Steps

Complete Setup

Review all setup steps to ensure everything is configured

Development Guide

Start building features with your configured backend

Resources

Build docs developers (and LLMs) love