> ## 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.

# Service Layer

> API integration services for external data and user library management

## Overview

EpiNeko uses a service layer pattern to abstract external API calls and data operations. This provides a clean separation between UI components and data fetching logic.

## Service Architecture

<CardGroup cols={2}>
  <Card title="jikan.ts" icon="database">
    Fetches anime data from MyAnimeList via Jikan API
  </Card>

  <Card title="library.ts" icon="bookmark">
    Manages user library with Supabase database
  </Card>
</CardGroup>

***

## Jikan Service

The Jikan service provides access to the [Jikan API v4](https://jikan.moe/), an unofficial MyAnimeList API.

### Base Configuration

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

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();
}
```

<Note>
  The service uses Next.js's `next.revalidate` for automatic cache management. Default cache time is 3600 seconds (1 hour).
</Note>

### Type Definitions

<Accordion title="JikanAnime Interface">
  ```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;
  }
  ```
</Accordion>

<Accordion title="JikanImage Interface">
  ```typescript theme={null}
  export interface JikanImage {
    image_url: string;
    small_image_url: string;
    large_image_url: string;
  }
  ```
</Accordion>

<Accordion title="JikanEpisode Interface">
  ```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;
  }
  ```
</Accordion>

<Accordion title="JikanResponse Interface">
  ```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;
      };
    };
  }
  ```
</Accordion>

### API Functions

#### getTopAnime

Fetch the top-rated anime from MyAnimeList.

```typescript theme={null}
export const getTopAnime = async (
  page = 1
): Promise<JikanResponse<JikanAnime[]>> => {
  return jikanFetch<JikanResponse<JikanAnime[]>>(
    `/top/anime?page=${page}`, 
    3600 // 1 hour cache
  );
};
```

**Usage:**

```tsx theme={null}
const res = await getTopAnime(1);
const animeList = res.data;
```

***

#### searchAnime

Search for anime by query string.

```typescript theme={null}
export const searchAnime = async (
  query: string, 
  page = 1
): Promise<JikanResponse<JikanAnime[]>> => {
  return jikanFetch<JikanResponse<JikanAnime[]>>(
    `/anime?q=${encodeURIComponent(query)}&page=${page}`, 
    3600
  );
};
```

**Usage:**

```tsx theme={null}
const results = await searchAnime("Naruto", 1);
const foundAnime = results.data;
```

<Tip>
  The query is automatically URL-encoded to handle special characters safely.
</Tip>

***

#### getAnimeById

Fetch detailed information for a specific anime.

```typescript theme={null}
export const getAnimeById = async (
  id: number
): Promise<JikanResponse<JikanAnime>> => {
  return jikanFetch<JikanResponse<JikanAnime>>(
    `/anime/${id}`, 
    86400 // 24 hours cache
  );
};
```

**Usage:**

```tsx theme={null}
const response = await getAnimeById(anime.mal_id);
const animeDetails = response.data;
```

<Note>
  Individual anime details are cached for 24 hours since they change infrequently.
</Note>

***

#### getAnimeCharacters

Fetch character list for an anime.

```typescript theme={null}
export const getAnimeCharacters = async (
  id: number
): Promise<JikanResponse<any[]>> => {
  return jikanFetch<JikanResponse<any[]>>(
    `/anime/${id}/characters`, 
    86400
  );
};
```

***

#### getAnimeEpisodes

Fetch episode list for an anime with pagination.

```typescript theme={null}
export const getAnimeEpisodes = async (
  id: number, 
  page = 1
): Promise<JikanResponse<JikanEpisode[]>> => {
  return jikanFetch<JikanResponse<JikanEpisode[]>>(
    `/anime/${id}/episodes?page=${page}`, 
    86400
  );
};
```

**Usage:**

```tsx theme={null}
const episodes = await getAnimeEpisodes(animeId);
const episodeList = episodes.data;
```

### Error Handling

The Jikan service includes built-in error handling:

<Steps>
  <Step title="Rate Limit Detection">
    Catches 429 status and returns a user-friendly message
  </Step>

  <Step title="HTTP Error Handling">
    Throws descriptive errors for failed requests
  </Step>

  <Step title="Automatic Retries">
    Next.js cache system handles stale data revalidation
  </Step>
</Steps>

***

## Library Service

The Library service manages user anime collections using Supabase.

### Type Definitions

```typescript theme={null}
export type LibraryStatus = 'watching' | 'completed' | 'dropped' | 'plan_to_watch';

export interface LibraryItem {
  id?: string;
  user_id?: string;
  anime_id_jikan: number;
  title: string;
  image_url?: string;
  status: LibraryStatus;
  score?: number;
  episodes_watched?: number;
}
```

### Database Operations

#### addToLibrary

Add a new anime to the user's library.

```typescript theme={null}
export const addToLibrary = async (item: LibraryItem) => {
  const supabase = createClient();
  const { data: { user } } = await supabase.auth.getUser();

  if (!user) throw new Error('User must be logged in to add to library');

  const { data, error } = await supabase
    .from('user_library')
    .insert([{ ...item, user_id: user.id }])
    .select()
    .single();

  if (error) throw error;
  return data;
};
```

**Usage:**

```tsx theme={null}
const item: LibraryItem = {
  anime_id_jikan: anime.mal_id,
  title: anime.title,
  image_url: anime.images.webp.large_image_url,
  status: 'plan_to_watch',
};

await addToLibrary(item);
```

<Warning>
  This function requires an authenticated user. It throws an error if called without authentication.
</Warning>

***

#### removeFromLibrary

Remove an anime from the user's library.

```typescript theme={null}
export const removeFromLibrary = async (animeIdJikan: number) => {
  const supabase = createClient();
  const { data: { user } } = await supabase.auth.getUser();

  if (!user) throw new Error('User must be logged in to remove from library');

  const { error } = await supabase
    .from('user_library')
    .delete()
    .match({ user_id: user.id, anime_id_jikan: animeIdJikan });

  if (error) throw error;
};
```

**Usage:**

```tsx theme={null}
await removeFromLibrary(anime.mal_id);
```

***

#### updateLibraryItem

Update an existing library entry.

```typescript theme={null}
export const updateLibraryItem = async (
  animeIdJikan: number, 
  updates: Partial<LibraryItem>
) => {
  const supabase = createClient();
  const { data: { user } } = await supabase.auth.getUser();

  if (!user) throw new Error('User must be logged in to update library');

  const { data, error } = await supabase
    .from('user_library')
    .update(updates)
    .match({ user_id: user.id, anime_id_jikan: animeIdJikan })
    .select()
    .single();

  if (error) throw error;
  return data;
};
```

**Usage:**

```tsx theme={null}
// Update watch status
await updateLibraryItem(animeId, { 
  status: 'watching',
  episodes_watched: 5
});

// Update score
await updateLibraryItem(animeId, { 
  score: 9
});
```

***

#### getLibrary

Retrieve all library items for the current user.

```typescript theme={null}
export const getLibrary = async () => {
  const supabase = createClient();
  const { data, error } = await supabase
    .from('user_library')
    .select('*')
    .order('updated_at', { ascending: false });

  if (error) throw error;
  return data;
};
```

**Usage:**

```tsx theme={null}
const myLibrary = await getLibrary();
// Returns array of LibraryItem objects
```

<Note>
  Results are automatically sorted by most recently updated first.
</Note>

***

#### getLibraryItem

Check if a specific anime is in the user's library.

```typescript theme={null}
export const getLibraryItem = async (animeIdJikan: number) => {
  const supabase = createClient();
  const { data: { user } } = await supabase.auth.getUser();

  if (!user) return null;

  const { data, error } = await supabase
    .from('user_library')
    .select('*')
    .match({ user_id: user.id, anime_id_jikan: animeIdJikan })
    .single();

  if (error && error.code !== 'PGRST116') throw error;
  return data;
};
```

**Usage:**

```tsx theme={null}
const libraryItem = await getLibraryItem(anime.mal_id);
const isInLibrary = !!libraryItem;
const currentStatus = libraryItem?.status;
```

<Tip>
  This function returns `null` for unauthenticated users instead of throwing an error, making it safe to call in any context.
</Tip>

***

## Supabase Utilities

### Client-Side Client

For browser-based operations:

```typescript src/utils/supabase/client.ts theme={null}
import { createBrowserClient } from '@supabase/ssr'

export function createClient() {
  const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL;
  const supabaseKey = process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY;

  if (!supabaseUrl || !supabaseKey) {
    throw new Error('Missing Supabase environment variables');
  }

  return createBrowserClient(supabaseUrl, supabaseKey);
}
```

***

### Server-Side Client

For server components and API routes:

```typescript src/utils/supabase/server.ts theme={null}
import { createServerClient } from '@supabase/ssr'
import { cookies } from 'next/headers'

export async function createClient() {
  const cookieStore = await cookies();

  return createServerClient(
    process.env.NEXT_PUBLIC_SUPABASE_URL!,
    process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!,
    {
      cookies: {
        getAll() {
          return cookieStore.getAll()
        },
        setAll(cookiesToSet) {
          try {
            cookiesToSet.forEach(({ name, value, options }) =>
              cookieStore.set(name, value, options)
            )
          } catch {
            // Ignore if called from Server Component
          }
        },
      },
    }
  );
}
```

***

### Middleware Client

For session management in middleware:

```typescript src/utils/supabase/middleware.ts theme={null}
import { createServerClient } from '@supabase/ssr'
import { NextResponse, type NextRequest } from 'next/server'

export async function updateSession(request: NextRequest) {
  let supabaseResponse = NextResponse.next({ request });

  const supabase = createServerClient(
    process.env.NEXT_PUBLIC_SUPABASE_URL!,
    process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!,
    {
      cookies: {
        getAll() {
          return request.cookies.getAll()
        },
        setAll(cookiesToSet) {
          cookiesToSet.forEach(({ name, value }) => 
            request.cookies.set(name, value)
          );
          supabaseResponse = NextResponse.next({ request });
          cookiesToSet.forEach(({ name, value, options }) =>
            supabaseResponse.cookies.set(name, value, options)
          );
        },
      },
    }
  );

  await supabase.auth.getUser();
  return supabaseResponse;
}
```

***

## Best Practices

<CardGroup cols={2}>
  <Card title="Error Handling" icon="shield">
    Always wrap service calls in try-catch blocks
  </Card>

  <Card title="Loading States" icon="spinner">
    Show feedback while waiting for API responses
  </Card>

  <Card title="Cache Strategy" icon="clock">
    Use appropriate revalidation times per endpoint
  </Card>

  <Card title="Authentication" icon="lock">
    Check user state before library operations
  </Card>
</CardGroup>

## Common Patterns

<Accordion title="Fetching with Loading State">
  ```tsx theme={null}
  const [data, setData] = useState<JikanAnime[]>([]);
  const [isLoading, setIsLoading] = useState(true);

  useEffect(() => {
    const fetchData = async () => {
      try {
        const res = await getTopAnime();
        setData(res.data);
      } catch (error) {
        console.error('Fetch error:', error);
      } finally {
        setIsLoading(false);
      }
    };
    
    fetchData();
  }, []);
  ```
</Accordion>

<Accordion title="Optimistic Updates">
  ```tsx theme={null}
  const handleToggle = async () => {
    // Update UI immediately
    setIsInLibrary(true);
    
    try {
      // Persist to database
      await addToLibrary(item);
    } catch (error) {
      // Revert on error
      setIsInLibrary(false);
      console.error(error);
    }
  };
  ```
</Accordion>

<Accordion title="Parallel Requests">
  ```tsx theme={null}
  const [episodes, library] = await Promise.all([
    getAnimeEpisodes(animeId),
    getLibraryItem(animeId)
  ]);
  ```
</Accordion>

## Related Documentation

<CardGroup cols={3}>
  <Card title="Components" icon="puzzle-piece" href="/development/components">
    See how components use services
  </Card>

  <Card title="Environment Variables" icon="key" href="/development/environment-variables">
    Configure API credentials
  </Card>

  <Card title="Project Structure" icon="folder-tree" href="/development/project-structure">
    Understand service organization
  </Card>
</CardGroup>
