> ## Documentation Index
> Fetch the complete documentation index at: https://devperez08-platform-list-anime-54.mintlify.site/llms.txt
> Use this file to discover all available pages before exploring further.

# Jikan API Integration

> Complete guide to using the Jikan MyAnimeList API v4 in EpiNeko

## Overview

EpiNeko integrates with the [Jikan API v4](https://jikan.moe/) to fetch anime data from MyAnimeList. The Jikan service provides a clean, typed interface with built-in caching and rate limit handling.

<Note>
  All Jikan API functions are located in `src/services/jikan.ts:1` and include automatic caching and error handling.
</Note>

## Core Functions

### getTopAnime

Fetches the top-rated anime from MyAnimeList with pagination support.

```typescript theme={null}
export const getTopAnime = async (page = 1): Promise<JikanResponse<JikanAnime[]>>
```

<ParamField path="page" type="number" default={1}>
  The page number for pagination
</ParamField>

**Cache Duration:** 1 hour (3600 seconds)

<CodeGroup>
  ```typescript Usage Example theme={null}
  import { getTopAnime } from '@/services/jikan';

  // Fetch first page of top anime
  const response = await getTopAnime(1);
  console.log(response.data); // Array of JikanAnime objects
  console.log(response.pagination); // Pagination metadata
  ```

  ```json Response Example theme={null}
  {
    "data": [
      {
        "mal_id": 5114,
        "url": "https://myanimelist.net/anime/5114",
        "images": {
          "jpg": {
            "image_url": "https://cdn.myanimelist.net/images/anime/...",
            "small_image_url": "https://cdn.myanimelist.net/images/anime/...",
            "large_image_url": "https://cdn.myanimelist.net/images/anime/..."
          },
          "webp": { /* ... */ }
        },
        "title": "Fullmetal Alchemist: Brotherhood",
        "title_english": "Fullmetal Alchemist: Brotherhood",
        "title_japanese": "鋼の錬金術師 FULLMETAL ALCHEMIST",
        "type": "TV",
        "episodes": 64,
        "status": "Finished Airing",
        "score": 9.09,
        "synopsis": "...",
        "season": "spring",
        "year": 2009
      }
    ],
    "pagination": {
      "last_visible_page": 100,
      "has_next_page": true,
      "current_page": 1,
      "items": {
        "count": 25,
        "total": 2500,
        "per_page": 25
      }
    }
  }
  ```
</CodeGroup>

***

### searchAnime

Search for anime by query string with pagination support.

```typescript theme={null}
export const searchAnime = async (query: string, page = 1): Promise<JikanResponse<JikanAnime[]>>
```

<ParamField path="query" type="string" required>
  The search query string (automatically URL-encoded)
</ParamField>

<ParamField path="page" type="number" default={1}>
  The page number for pagination
</ParamField>

**Cache Duration:** 1 hour (3600 seconds)

<CodeGroup>
  ```typescript Usage Example theme={null}
  import { searchAnime } from '@/services/jikan';

  // Search for anime
  const results = await searchAnime('Naruto', 1);

  // Search handles special characters
  const results2 = await searchAnime('Re:Zero', 1);
  ```

  ```typescript Server Component theme={null}
  // app/search/page.tsx
  import { searchAnime } from '@/services/jikan';

  export default async function SearchPage({
    searchParams,
  }: {
    searchParams: { q: string; page?: string }
  }) {
    const results = await searchAnime(
      searchParams.q,
      parseInt(searchParams.page || '1')
    );

    return (
      <div>
        {results.data.map((anime) => (
          <AnimeCard key={anime.mal_id} anime={anime} />
        ))}
      </div>
    );
  }
  ```
</CodeGroup>

***

### getAnimeById

Fetch detailed information about a specific anime by its MyAnimeList ID.

```typescript theme={null}
export const getAnimeById = async (id: number): Promise<JikanResponse<JikanAnime>>
```

<ParamField path="id" type="number" required>
  The MyAnimeList ID of the anime
</ParamField>

**Cache Duration:** 24 hours (86400 seconds)

<CodeGroup>
  ```typescript Usage Example theme={null}
  import { getAnimeById } from '@/services/jikan';

  const response = await getAnimeById(5114);
  const anime = response.data;

  console.log(anime.title); // "Fullmetal Alchemist: Brotherhood"
  console.log(anime.score); // 9.09
  console.log(anime.episodes); // 64
  ```

  ```typescript Dynamic Route theme={null}
  // app/anime/[id]/page.tsx
  import { getAnimeById } from '@/services/jikan';

  export default async function AnimePage({
    params,
  }: {
    params: { id: string }
  }) {
    const response = await getAnimeById(parseInt(params.id));
    const anime = response.data;

    return (
      <div>
        <h1>{anime.title}</h1>
        <p>Score: {anime.score}</p>
        <p>{anime.synopsis}</p>
      </div>
    );
  }
  ```
</CodeGroup>

***

### getAnimeCharacters

Fetch the characters for a specific anime.

```typescript theme={null}
export const getAnimeCharacters = async (id: number): Promise<JikanResponse<any[]>>
```

<ParamField path="id" type="number" required>
  The MyAnimeList ID of the anime
</ParamField>

**Cache Duration:** 24 hours (86400 seconds)

```typescript theme={null}
import { getAnimeCharacters } from '@/services/jikan';

const response = await getAnimeCharacters(5114);
const characters = response.data;
```

***

### getAnimeEpisodes

Fetch episode information for a specific anime with pagination.

```typescript theme={null}
export const getAnimeEpisodes = async (id: number, page = 1): Promise<JikanResponse<JikanEpisode[]>>
```

<ParamField path="id" type="number" required>
  The MyAnimeList ID of the anime
</ParamField>

<ParamField path="page" type="number" default={1}>
  The page number for pagination
</ParamField>

**Cache Duration:** 24 hours (86400 seconds)

<CodeGroup>
  ```typescript Usage Example theme={null}
  import { getAnimeEpisodes } from '@/services/jikan';

  const response = await getAnimeEpisodes(5114, 1);
  const episodes = response.data;

  episodes.forEach((episode) => {
    console.log(`Episode ${episode.mal_id}: ${episode.title}`);
    console.log(`Filler: ${episode.filler}, Recap: ${episode.recap}`);
  });
  ```

  ```typescript Episode List Component theme={null}
  export default async function EpisodeList({ animeId }: { animeId: number }) {
    const response = await getAnimeEpisodes(animeId);
    
    return (
      <div>
        {response.data.map((episode) => (
          <div key={episode.mal_id}>
            <h3>{episode.title}</h3>
            {episode.title_japanese && <p>{episode.title_japanese}</p>}
            {episode.filler && <span>Filler</span>}
            {episode.recap && <span>Recap</span>}
          </div>
        ))}
      </div>
    );
  }
  ```
</CodeGroup>

## TypeScript Interfaces

### JikanAnime

```typescript theme={null}
export interface JikanAnime {
  mal_id: number;
  url: string;
  images: {
    jpg: JikanImage;
    webp: JikanImage;
  };
  title: string;
  title_english: string | null;
  title_japanese: string | null;
  type: string;
  episodes: number | null;
  status: string;
  score: number | null;
  synopsis: string | null;
  background: string | null;
  season: string | null;
  year: number | null;
}
```

<Expandable title="Field Descriptions">
  <ResponseField name="mal_id" type="number" required>
    The unique MyAnimeList ID
  </ResponseField>

  <ResponseField name="url" type="string" required>
    Direct URL to the anime on MyAnimeList
  </ResponseField>

  <ResponseField name="images" type="object" required>
    Contains JPG and WebP image variants with multiple sizes
  </ResponseField>

  <ResponseField name="title" type="string" required>
    The main title of the anime
  </ResponseField>

  <ResponseField name="title_english" type="string | null">
    English translated title (may be null)
  </ResponseField>

  <ResponseField name="title_japanese" type="string | null">
    Original Japanese title (may be null)
  </ResponseField>

  <ResponseField name="type" type="string" required>
    Anime type: TV, Movie, OVA, Special, ONA, Music
  </ResponseField>

  <ResponseField name="episodes" type="number | null">
    Total number of episodes (null for ongoing/unknown)
  </ResponseField>

  <ResponseField name="status" type="string" required>
    Current airing status
  </ResponseField>

  <ResponseField name="score" type="number | null">
    MyAnimeList user score (0-10 scale)
  </ResponseField>

  <ResponseField name="synopsis" type="string | null">
    Anime description/synopsis
  </ResponseField>

  <ResponseField name="season" type="string | null">
    Airing season: spring, summer, fall, winter
  </ResponseField>

  <ResponseField name="year" type="number | null">
    Year of airing
  </ResponseField>
</Expandable>

### JikanImage

```typescript theme={null}
export interface JikanImage {
  image_url: string;
  small_image_url: string;
  large_image_url: string;
}
```

### JikanEpisode

```typescript theme={null}
export interface JikanEpisode {
  mal_id: number;
  url: string;
  title: string;
  title_japanese: string | null;
  title_romanji: string | null;
  duration: number | null;
  aired: string | null;
  filler: boolean;
  recap: boolean;
  forum_url: string | null;
}
```

<Expandable title="Episode Field Descriptions">
  <ResponseField name="filler" type="boolean" required>
    Indicates if the episode is filler content (not from source material)
  </ResponseField>

  <ResponseField name="recap" type="boolean" required>
    Indicates if the episode is a recap episode
  </ResponseField>

  <ResponseField name="duration" type="number | null">
    Episode duration in seconds
  </ResponseField>
</Expandable>

### JikanResponse\<T>

```typescript theme={null}
export interface JikanResponse<T> {
  data: T;
  pagination?: {
    last_visible_page: number;
    has_next_page: boolean;
    current_page: number;
    items: {
      count: number;
      total: number;
      per_page: number;
    };
  };
}
```

## Rate Limiting & Error Handling

The Jikan API has rate limits. The service automatically handles rate limit errors:

```typescript theme={null}
// src/services/jikan.ts:48
if (response.status === 429) {
  throw new Error('Jikan API rate limit exceeded. Please try again later.');
}
```

<Warning>
  When you receive a 429 error, the Jikan API is temporarily unavailable. The application should display a user-friendly message and retry the request after a delay.
</Warning>

## Caching Strategy

All Jikan API calls use Next.js ISR (Incremental Static Regeneration) for caching:

| Function             | Cache Duration | Reason                          |
| -------------------- | -------------- | ------------------------------- |
| `getTopAnime`        | 1 hour         | Top lists change frequently     |
| `searchAnime`        | 1 hour         | Search results may update       |
| `getAnimeById`       | 24 hours       | Individual anime data is stable |
| `getAnimeCharacters` | 24 hours       | Character lists rarely change   |
| `getAnimeEpisodes`   | 24 hours       | Episode data is stable          |

```typescript theme={null}
// src/services/jikan.ts:43
async function jikanFetch<T>(endpoint: string, revalidate = 3600): Promise<T> {
  const response = await fetch(`${JIKAN_API_BASE}${endpoint}`, {
    next: { revalidate: revalidate }
  });
  // ...
}
```

<Note>
  You can adjust cache durations by modifying the `revalidate` parameter in each function call.
</Note>

## Best Practices

<Expandable title="1. Handle Loading States">
  Always show loading states when fetching data from the Jikan API:

  ```typescript theme={null}
  'use client';
  import { useState, useEffect } from 'react';
  import { getTopAnime } from '@/services/jikan';

  export default function TopAnimeList() {
    const [anime, setAnime] = useState([]);
    const [loading, setLoading] = useState(true);

    useEffect(() => {
      getTopAnime().then(response => {
        setAnime(response.data);
        setLoading(false);
      });
    }, []);

    if (loading) return <div>Loading...</div>;
    return <div>{/* render anime */}</div>;
  }
  ```
</Expandable>

<Expandable title="2. Error Handling">
  Always wrap API calls in try-catch blocks:

  ```typescript theme={null}
  try {
    const response = await getAnimeById(id);
    return response.data;
  } catch (error) {
    if (error.message.includes('rate limit')) {
      // Show rate limit message
    } else {
      // Show generic error
    }
  }
  ```
</Expandable>

<Expandable title="3. Type Safety">
  Always use the provided TypeScript interfaces:

  ```typescript theme={null}
  import type { JikanAnime, JikanResponse } from '@/services/jikan';

  function processAnime(anime: JikanAnime) {
    // TypeScript will ensure type safety
    console.log(anime.mal_id);
  }
  ```
</Expandable>

## API Base URL

```typescript theme={null}
const JIKAN_API_BASE = 'https://api.jikan.moe/v4';
```

All requests are made to the Jikan API v4 endpoint. No API key is required.

## Related Resources

<Card title="Jikan API Documentation" icon="book" href="https://docs.api.jikan.moe/">
  Official Jikan API v4 documentation
</Card>

<Card title="MyAnimeList" icon="database" href="https://myanimelist.net/">
  Source data for all anime information
</Card>
