Skip to main content

Overview

EpiNeko’s discovery system helps users find their next favorite anime through powerful search capabilities and curated trending lists. All anime data is fetched from the Jikan API (unofficial MyAnimeList API), providing access to a vast database of anime information.

Real-time Search

Instant search results as you type with debouncing

Top Anime

Browse the highest-rated anime on MyAnimeList

Rich Metadata

Access titles, scores, synopses, images, and more

Pagination

Navigate through extensive anime catalogs

Jikan API Integration

EpiNeko uses the Jikan v4 API to fetch anime data from MyAnimeList:
const JIKAN_API_BASE = 'https://api.jikan.moe/v4';

// Generic fetch function with caching
async function jikanFetch<T>(endpoint: string, revalidate = 3600): Promise<T> {
  const response = await fetch(`${JIKAN_API_BASE}${endpoint}`, {
    next: { revalidate: revalidate }
  });

  if (!response.ok) {
    if (response.status === 429) {
      throw new Error('Jikan API rate limit exceeded. Please try again later.');
    }
    throw new Error(`Jikan API error: ${response.statusText}`);
  }

  return response.json();
}
Key Features (src/services/jikan.ts:43-56):
  • Built-in rate limit handling (429 errors)
  • Next.js cache revalidation for performance
  • Type-safe responses with TypeScript interfaces
  • Automatic error handling

Data Structures

Anime Object

The complete anime data structure returned by Jikan:
export interface JikanAnime {
  mal_id: number;                      // MyAnimeList ID (primary identifier)
  url: string;                         // MAL URL
  images: {
    jpg: JikanImage;
    webp: JikanImage;
  };
  title: string;                       // Romaji title
  title_english: string | null;        // English title
  title_japanese: string | null;       // Japanese title
  type: string;                        // TV, Movie, OVA, etc.
  episodes: number | null;             // Total episode count
  status: string;                      // Airing status
  score: number | null;                // MAL score (0-10)
  synopsis: string | null;             // Description
  background: string | null;           // Additional context
  season: string | null;               // Spring, Summer, Fall, Winter
  year: number | null;                 // Release year
}

Image Object

Multiple image sizes for optimal performance:
export interface JikanImage {
  image_url: string;          // Default size
  small_image_url: string;    // Thumbnail
  large_image_url: string;    // High resolution
}

Paginated Response

All list endpoints return paginated data:
export interface JikanResponse<T> {
  data: T;
  pagination?: {
    last_visible_page: number;
    has_next_page: boolean;
    current_page: number;
    items: {
      count: number;      // Items on current page
      total: number;      // Total items
      per_page: number;   // Items per page
    };
  };
}

Core API Functions

Search Anime

Search for anime by title with pagination support:
import { searchAnime } from '@/services/jikan';

// Basic search
const results = await searchAnime('naruto');
console.log(results.data); // Array of matching anime

// With pagination
const page2 = await searchAnime('naruto', 2);

// Access pagination info
if (results.pagination?.has_next_page) {
  console.log(`Page ${results.pagination.current_page} of ${results.pagination.last_visible_page}`);
}
Function Signature (src/services/jikan.ts:62-64):
export const searchAnime = async (
  query: string, 
  page = 1
): Promise<JikanResponse<JikanAnime[]>>
Search queries are automatically URL-encoded. Results are cached for 1 hour (3600 seconds) to reduce API calls.

Get Top Anime

Retrieve the highest-rated anime from MyAnimeList:
import { getTopAnime } from '@/services/jikan';

// Get first page of top anime
const topAnime = await getTopAnime();

// Display on homepage
topAnime.data.forEach(anime => {
  console.log(`${anime.title} - ★${anime.score}`);
});

// Load more pages
const nextPage = await getTopAnime(2);
Implementation (src/services/jikan.ts:58-60):
  • Returns 25 anime per page
  • Sorted by MAL score
  • Cached for 1 hour

Get Anime Details

Fetch complete information for a specific anime:
import { getAnimeById } from '@/services/jikan';

// Get by MyAnimeList ID
const anime = await getAnimeById(52991);

console.log(anime.data.title);        // "Sousou no Frieren"
console.log(anime.data.episodes);     // 28
console.log(anime.data.score);        // 9.39
console.log(anime.data.synopsis);     // Full description
Caching (src/services/jikan.ts:66-68):
  • Details are cached for 24 hours (86400 seconds)
  • Longer cache duration since anime details rarely change

Get Anime Characters

Retrieve character information for an anime:
import { getAnimeCharacters } from '@/services/jikan';

const characters = await getAnimeCharacters(52991);

characters.data.forEach(char => {
  console.log(char.character.name);
  console.log(char.role); // Main, Supporting
  console.log(char.voice_actors);
});

Get Anime Episodes

Fetch episode lists with titles and metadata:
import { getAnimeEpisodes } from '@/services/jikan';

const episodes = await getAnimeEpisodes(52991);

episodes.data.forEach(ep => {
  console.log(`Episode ${ep.mal_id}: ${ep.title}`);
  console.log(`Aired: ${ep.aired}`);
  console.log(`Filler: ${ep.filler}`);
  console.log(`Recap: ${ep.recap}`);
});
Episode Structure (src/services/jikan.ts:74-85):
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;
}

UI Components

SearchBar Component

Real-time search with autocomplete dropdown:
import SearchBar from '@/components/anime/SearchBar';

// Add to navigation or header
<SearchBar />
1

User Types Query

Input is debounced by 500ms to avoid excessive API calls
2

Minimum Length

Search triggers when query length exceeds 2 characters
3

API Call

Fetches top 5 matching results from Jikan
4

Display Results

Shows anime cards with thumbnail, title, type, and score
5

Click Result

Navigates to anime detail page or closes dropdown
Key Features (src/components/anime/SearchBar.tsx):
  • Click-outside detection to close dropdown
  • Loading spinner during search
  • “No results” message when appropriate
  • “View all results” link for full search page

AnimeCard Component

Reusable card component for displaying anime in grids:
import AnimeCard from '@/components/anime/AnimeCard';

<AnimeCard 
  mal_id={52991}
  image="https://cdn.myanimelist.net/images/anime/..."
  title="Sousou no Frieren"
  score="9.39"
/>
Visual Effects (src/components/anime/AnimeCard.tsx:14-46):
  • Hover scale animation (1.02x)
  • Image zoom effect on hover
  • Primary color glow border
  • Rating badge overlay
  • Gradient overlay on hover
  • Fallback placeholder if image fails

Homepage Discovery

The homepage showcases trending anime with a hero section:
// Features the #1 trending anime
// Large background image with gradient overlays
// Title, synopsis, and CTA buttons
// Auto-populated from getTopAnime()
Located at src/app/page.tsx:31-79

Caching Strategy

EpiNeko implements intelligent caching to balance freshness with performance:

Search Results

1 hour cacheBalances freshness with reduced API load for popular searches

Top Anime

1 hour cacheRankings change slowly, hourly updates are sufficient

Anime Details

24 hour cacheIndividual anime data is static, rarely needs updating

Rate Limiting

The Jikan API has rate limits to protect MyAnimeList servers:
Rate Limit: 60 requests per minute, 3 requests per secondEpiNeko automatically detects 429 errors and displays user-friendly messages. Implement client-side throttling for heavy usage patterns.
// Rate limit handling (src/services/jikan.ts:49-52)
if (response.status === 429) {
  throw new Error('Jikan API rate limit exceeded. Please try again later.');
}
Best Practices:
  • Use cached data when possible
  • Debounce search inputs (500ms minimum)
  • Implement loading states during API calls
  • Queue requests if making multiple calls
  • Consider implementing request retry logic with exponential backoff

Error Handling

Robust error handling for network and API issues:
try {
  const results = await searchAnime('naruto');
} catch (error) {
  if (error.message.includes('rate limit')) {
    // Display rate limit message
    alert('Too many requests. Please wait a moment.');
  } else if (error.message.includes('API error')) {
    // API returned an error status
    alert('Service temporarily unavailable');
  } else {
    // Network error
    alert('Connection failed. Check your internet.');
  }
}

Search Optimization

Tips for implementing efficient search:
1

Debounce Input

Wait 300-500ms after user stops typing before triggering search
const timer = setTimeout(async () => {
  if (query.length > 2) {
    await searchAnime(query);
  }
}, 500);
2

Limit Results

Only show top 5-10 results in autocomplete dropdowns
const results = await searchAnime(query);
setDisplayed(results.data.slice(0, 5));
3

Cancel Previous

Abort in-flight requests when new search starts
const controller = new AbortController();
// Cancel on new search
4

Show Loading

Display loading indicators during API calls for better UX