API Deposu
MarketplaceOpen catalogBlogMCPTest LabSubmit APISupportGitHub

API Deposu

This catalog was verified from public sources as of October 27, 2026. Always review official documentation before integration.

iletisim@apideposu.com

Trust and content

AboutEditorial policyMethodologyContactSupportCredits

Legal and preferences

Terms of ServicePrivacyCookiesX
Back to blog
Open Data
HOW_TO
anilist-graphql-api

Build an Anime Search App with AniList GraphQL (2026)

A practical Next.js tutorial: search AniList's GraphQL API, render covers and scores, handle empty states, and cache results without an API key.

Oct 27, 20264 min read628 wordsAuthor:Mert Murat

An anime search screen looks simple: one input, a grid of covers, and a detail page. The hard part is not the UI. It is keeping the query shape small, handling empty and error states, and avoiding a request on every keystroke.

This tutorial builds the data layer and a small Next.js UI with AniList's GraphQL API. It needs no API key, but it does need a deliberate request shape and caching.

What we are building

  • a search box that accepts an anime title
  • a server route that validates the query and calls AniList
  • a results grid with cover, title, score, episodes, and status
  • a detail view that requests one title by its AniList id
  • loading, empty, and error states

The example uses Next.js App Router, but the API layer works in any Node.js server.

1. Test the query before building UI

On 2026-10-27, this keyless search: "naruto" query returned HTTP 200 with six results:

bash
curl -s https://graphql.anilist.co \
  -H "content-type: application/json" \
  --data '{
    "query": "query Search($search: String) { Page(perPage: 6) { media(type: ANIME, search: $search, sort: SEARCH_MATCH) { id title { romaji english } coverImage { large } averageScore episodes status } } }",
    "variables": { "search": "naruto" }
  }'

The response shape we care about:

ts
type AnimeSearchResponse = {
  data: {
    Page: {
      media: Array<{
        id: number;
        title: { romaji: string | null; english: string | null };
        coverImage: { large: string | null } | null;
        averageScore: number | null;
        episodes: number | null;
        status: string;
      }>;
    };
  };
  errors?: Array<{ message: string }>;
};

averageScore and episodes can be null; do not render them as 0. A missing score is not a zero score.

2. Put AniList behind a server route

Create a route such as app/api/anime/search/route.ts:

ts
import { NextRequest, NextResponse } from "next/server";

const ANILIST_ENDPOINT = "https://graphql.anilist.co";

const SEARCH_QUERY = `
  query Search($search: String) {
    Page(perPage: 12) {
      media(type: ANIME, search: $search, sort: SEARCH_MATCH) {
        id
        title { romaji english }
        coverImage { large }
        averageScore
        episodes
        status
      }
    }
  }
`;

export async function GET(request: NextRequest) {
  const search = request.nextUrl.searchParams.get("q")?.trim() ?? "";

  if (search.length < 2 || search.length > 80) {
    return NextResponse.json(
      { error: "Query must be between 2 and 80 characters." },
      { status: 400 },
    );
  }

  const response = await fetch(ANILIST_ENDPOINT, {
    method: "POST",
    headers: {
      "content-type": "application/json",
      accept: "application/json",
    },
    body: JSON.stringify({
      query: SEARCH_QUERY,
      variables: { search },
    }),
    next: { revalidate: 300 },
  });

  if (!response.ok) {
    return NextResponse.json(
      { error: `AniList returned ${response.status}` },
      { status: 502 },
    );
  }

  const payload = await response.json();

  if (payload.errors?.length) {
    return NextResponse.json(
      { error: payload.errors[0].message },
      { status: 502 },
    );
  }

  return NextResponse.json(payload);
}

Why a server route instead of calling AniList from the component?

  • the query and field list stay in one place
  • input validation happens before the upstream call
  • you can cache responses without relying on browser memory
  • you can add Jikan or MangaDex as a fallback later

Do not proxy arbitrary GraphQL text from the client. That turns your route into an open relay and makes rate-limit abuse easy. Accept a search string and send a fixed query.

3. Build the search box

tsx
"use client";

import { useEffect, useState } from "react";

type Anime = {
  id: number;
  title: { romaji: string | null; english: string | null };
  coverImage: { large: string | null } | null;
  averageScore: number | null;
  episodes: number | null;
  status: string;
};

export function AnimeSearch() {
  const [query, setQuery] = useState("");
  const [items, setItems] = useState<Anime[]>([]);
  const [loading, setLoading] = useState(false);
  const [error, setError] = useState<string | null>(null);

  useEffect(() => {
    const trimmed = query.trim();
    if (trimmed.length < 2) {
      setItems([]);
      setError(null);
      return;
    }

    const controller = new AbortController();
    const timer = window.setTimeout(async () => {
      setLoading(true);
      setError(null);

      try {
        const response = await fetch(
          `/api/anime/search?q=${encodeURIComponent(trimmed)}`,
          { signal: controller.signal },
        );
        const payload = await response.json();
        if (!response.ok) throw new Error(payload.error ?? "Search failed");
        setItems(payload.data?.Page?.media ?? []);
      } catch (requestError) {
        if (requestError instanceof DOMException && requestError.name === "AbortError") {
          return;
        }
        setError(requestError instanceof Error ? requestError.message : "Search failed");
        setItems([]);
      } finally {
        setLoading(false);
      }
    }, 300);

    return () => {
      window.clearTimeout(timer);
      controller.abort();
    };
  }, [query]);

  return (
    <section>
      <label htmlFor="anime-search">Search anime</label>
      <input
        id="anime-search"
        value={query}
        onChange={(event) => setQuery(event.target.value)}
        placeholder="Try: Frieren"
      />

      {loading ? <p>Searching…</p> : null}
      {error ? <p role="alert">{error}</p> : null}
      {!loading && !error && query.trim().length >= 2 && items.length === 0 ? (
        <p>No titles matched. Try a shorter title or the Japanese name.</p>
      ) : null}

      <ul>
        {items.map((anime) => (
          <li key={anime.id}>
            {anime.coverImage?.large ? (
              <img
                src={anime.coverImage.large}
                alt=""
                width={120}
                height={170}
                loading="lazy"
              />
            ) : null}
            <h2>{anime.title.english ?? anime.title.romaji}</h2>
            <p>
              {anime.averageScore != null ? `${anime.averageScore}/100` : "No score"}
              {anime.episodes != null ? ` · ${anime.episodes} episodes` : ""}
            </p>
            <p>{anime.status.replaceAll("_", " ").toLowerCase()}</p>
          </li>
        ))}
      </ul>
    </section>
  );
}

The debounce and AbortController are not optional polish. Without them, a fast typist can trigger several upstream requests and render results out of order.

4. Add a detail query

For the detail page, request only the fields the page can render:

ts
const DETAIL_QUERY = `
  query Detail($id: Int) {
    Media(id: $id, type: ANIME) {
      id
      title { romaji english }
      description(asHtml: false)
      coverImage { large }
      bannerImage
      averageScore
      episodes
      duration
      status
      genres
      startDate { year month day }
    }
  }
`;

Pass id as a number, reject non-integers, and handle a null Media as a 404 rather than rendering an empty shell.

5. Cache and rate limits

AniList is free and keyless, which makes it easy to abuse accidentally. Set a short cache on repeated searches, do not request on every keystroke, and back off when the API returns 429.

A simple policy:

  • cache the same normalized query for 5 minutes
  • debounce the input by 300 ms
  • cap perPage at 12–20
  • log upstream status codes and query latency
  • show a retry action instead of silently returning an empty list

Check AniList's current rate-limit documentation before launch; limits can change. The same discipline applies to the alternatives below.

Alternatives

  • Jikan if you want REST and MyAnimeList-shaped

resources.

  • MangaDex when the product is manga-first and

needs chapter/translation data.

  • AniList remains the best fit when you want one flexible GraphQL request for

search and detail cards.

Before you ship

A search screen is a small product surface, so the operational details matter more than the query itself. Add a visible retry path for upstream failures, keep a request id in your logs so a bad result can be traced, and make the empty state distinguish “no match” from “the API failed.” Test with a title that has an English and a romaji name, and with a title that returns no rows.

Images are remote assets: use a stable image component, provide dimensions to avoid layout shift, and do not proxy or rehost covers without checking the provider's terms. If you add a detail page, cache it separately from search results; a detail response changes less often than a search query.

Finally, keep the search route behind your own validation and rate limit. A keyless upstream is not an invitation to let anonymous clients call it without bounds.

Related API Deposu entries

  • AniList GraphQL API
  • Jikan REST API
  • MangaDex API

Sources

  • AniList GraphQL documentation
  • Jikan REST API documentation
  • MangaDex API documentation

Frequently Asked Questions

›Does AniList require an API key?

No. AniList's public GraphQL API works without an API key for read queries. You still need to respect its rate limit and send well-formed queries.

›Should I call AniList directly from the browser?

For a small prototype it works, but a server-side route gives you caching, request validation, rate-limit protection, and one place to add a fallback. In Next.js, use a route handler or server action rather than exposing every query to the client.

›How do I search anime by title in AniList?

Use the Page query with media(type: ANIME, search: $search, sort: SEARCH_MATCH). Request only the fields your cards render: id, title, coverImage, averageScore, episodes, and status.

›Is AniList GraphQL better than Jikan REST?

AniList is more flexible when you want to choose exact fields and combine related data in one request. Jikan is easier if you prefer REST and MyAnimeList-shaped resources. Both are keyless; pick based on the data and query style you need.

Author and editorial review information

Author:Mert Murat

Mert Murat created and maintains API Deposu — verifying the catalog's 400+ public API records against provider documentation, running endpoints where the API supports it, and writing the guides on this blog. Corrections and suggestions: iletisim@apideposu.com

Review method: Core claims were checked against the linked provider documentation and relevant API Deposu catalog records. Read the editorial policy

How this was made: AI assistance may be used while drafting. Before publication an editor checks every claim against the official provider documentation linked in the article and against the catalog records; anything that cannot be verified is not published.

Article info

min read4
words628
Related APIs3

Related APIs

anilist-graphql-api
jikan-rest-api
mangadex-api

Sources

AniList GraphQL documentation

https://docs.anilist.co/

Jikan REST API documentation

https://docs.api.jikan.moe/

MangaDex API documentation

https://api.mangadex.org/docs/

Related posts

Explore all blog posts
Continue reading
Best Free Anime and Manga APIs for Developers in 2026 cover image

Best Free Anime and Manga APIs for Developers in 2026

A practical shortlist of free anime and manga APIs covering search, discovery, libraries, and GraphQL — all without API keys.

Continue reading
Best Fun APIs for Side Projects and Demos in 2026 cover image

Best Fun APIs for Side Projects and Demos in 2026

A practical shortlist of free, keyless fun APIs — jokes, trivia, quotes, and advice — perfect for tutorials, demos, and learning projects.

Continue reading
How to Build a Weather Widget with Open-Meteo and Next.js cover image

How to Build a Weather Widget with Open-Meteo and Next.js

A practical Next.js tutorial for building a server-rendered weather widget with the free, keyless Open-Meteo API.

Explore more API guides

Browse API Deposu guides backed by real catalog data, practical comparisons, and developer-focused implementation notes.

Explore all blog posts