Jikan is the API behind nearly every "build an anime search app" tutorial โ free, no key, the whole MyAnimeList catalogue. It's also, by its own description, an unofficial API: a community-run cache that scrapes MAL on your behalf. That architecture has a failure mode you will eventually meet. While writing this guide (September 2026), the classic tutorial call failed:
$ curl "https://api.jikan.moe/v4/anime?q=naruto"
HTTP/2 504
{"status":504,"type":"BadResponseException","message":"Jikan failed to connect
to MyAnimeList. MyAnimeList may be down\/unavailable or refuses to connect",
"error":null}
At the same moment, /v4/anime/1 answered 200 in under a second โ because it was in Jikan's cache. That's the trap in one screenshot: cache hit = fast 200, cache miss = whatever mood MyAnimeList is in right now. Your search box works all afternoon, then 504s during the demo, because "Naruto season 4 page 3" fell out of cache. On top of that, Jikan's docs set a rate limit of 60 requests/minute and 3 requests/second โ easy to trip from a hot-reloading dev server or a parallel test suite. And the official MyAnimeList API isn't a quick fallback: it wants an OAuth2 app registration and token flow before your first request.
None of this is a knock on Jikan โ it's a genuinely great community project, and for production anime data it's the right choice. But your dev loop and CI shouldn't depend on MAL's uptime or share a 60/min budget. They need the exact response shapes, deterministically. That's a mock.
Pull Jikan's top-anime roster one time and host it as your own API (records keep every field verbatim โ mal_id, score, rank, image URLs, the nested titles array, all of it):
curl -s "https://api.jikan.moe/v4/top/anime" | jq '{anime: .data}' > db.json
curl -s -X POST "https://mockbird.mockbird.workers.dev/api/projects/import?name=jikan-mock" \
-H 'content-type: application/json' --data-binary @db.json
No signup, no key. The response has your project id and adminKey, plus honest warnings โ nested objects like images and aired are kept verbatim in the records but skipped from the flat field schema. Your copy answers instantly, from anywhere, with a query toolkit Jikan doesn't have:
$ curl "https://mockbird.mockbird.workers.dev/m/YOUR_ID/anime?title_like=frieren&select=title,score,rank"
[{"id":1,"title":"Sousou no Frieren","score":9.26,"rank":1}]
$ curl "https://mockbird.mockbird.workers.dev/m/YOUR_ID/anime?score_gte=8.9&select=title,score"
[{"title":"Sousou no Frieren","score":9.26},
{"title":"Fullmetal Alchemist: Brotherhood","score":9.11}, โฆ]
$ curl "https://mockbird.mockbird.workers.dev/m/YOUR_ID/anime?sortBy=rank&order=asc&limit=3&select=mal_id,title,rank"
Free bonuses: /m/YOUR_ID/types.ts generates TypeScript interfaces from the imported schema (?format=zod for Zod), and /graphql serves the same records with typed queries.
Jikan wraps every list in {"pagination":{โฆ},"data":[โฆ]}, and your frontend almost certainly reads resp.data and resp.pagination.has_next_page. Make the mock answer in that exact shape โ set it once as the project default with a response envelope template:
curl -X PUT "https://mockbird.mockbird.workers.dev/api/projects/YOUR_ID/settings" \
-H "x-admin-key: YOUR_ADMIN_KEY" -H 'content-type: application/json' \
-d '{"envelope":{"pagination":{"has_next_page":"$hasMore","current_page":"$page","items":{"count":"$count","total":"$total","per_page":"$limit"}},"data":"$data"}}'
Now every list response is Jikan-shaped (we verified this output on a live project while writing the guide):
$ curl "https://mockbird.mockbird.workers.dev/m/YOUR_ID/anime?limit=2&page=1"
{"pagination":{"has_next_page":true,"current_page":1,
"items":{"count":2,"total":25,"per_page":2}},
"data":[{"mal_id":52991,"title":"Sousou no Frieren",โฆ},{โฆ}]}
Your parsing code runs unchanged. (Jikan's last_visible_page isn't in the template vocabulary โ if your code depends on it, compute it from total and per_page, or pin a custom route.)
If your code hardcodes Jikan's /v4/โฆ paths, reproduce them as custom routes. Capture the real detail endpoint and serve it back byte-for-byte โ we verified the served response parse-equal to the live API's while writing this:
curl -s "https://api.jikan.moe/v4/anime/1" > anime1.json
jq '{path:"/v4/anime/1", body:.}' anime1.json \
| curl -s -X POST "https://mockbird.mockbird.workers.dev/api/projects/YOUR_ID/routes" \
-H "x-admin-key: YOUR_ADMIN_KEY" -H 'content-type: application/json' --data-binary @-
$ curl "https://mockbird.mockbird.workers.dev/m/YOUR_ID/v4/anime/1"
{"data":{"mal_id":1,"url":"https://myanimelist.net/anime/1/Cowboy_Bebop",โฆ}}
Route bodies are static โ good for detail pages and fixtures. For anything you want to query, use the /anime resource from ยง1.
The best reason to mock a fragile upstream: you can finally test the failure. Your error handling has almost certainly never seen Jikan's real 504 body in CI. Serve it verbatim โ this is the exact payload we captured up top (note: the body must be a single line; a raw newline inside a JSON string is invalid JSON):
curl -X POST "https://mockbird.mockbird.workers.dev/api/projects/YOUR_ID/routes" \
-H "x-admin-key: YOUR_ADMIN_KEY" -H 'content-type: application/json' \
-d '{"path":"/drills/504","status":504,"body":{"status":504,"type":"BadResponseException","message":"Jikan failed to connect to MyAnimeList. MyAnimeList may be down/unavailable or refuses to connect","error":null}}'
Now expect(err.type).toBe("BadResponseException") runs against the same bytes production will see. For retry logic, ?mock_seq=504,504,200 fails exactly twice then succeeds โ assert your third attempt renders. And ?mock_ratelimit=3 is Jikan's 60/min budget in miniature: three 200s with decrementing x-ratelimit-remaining, then a 429 with a real Retry-After to point your backoff at. More recipes: rate-limit simulation, loading & error states.
| Jikan | MAL official API | Your mock | |
|---|---|---|---|
| Real, current MAL data | โ (cached 24h-ish) | โ | โ frozen at capture โ by design |
| Signup / key | none | OAuth2 app registration | none |
| Catalogue | 30,000+ anime, manga, characters, seasons | whatever you import | |
| Uptime coupling | cache miss โ depends on MAL right now | MAL itself | independent |
| Rate limit | 60/min, 3/sec | per-app | 10,000/day per project |
| Deterministic for tests | โ rankings move, cache varies | โ | โ |
| Can simulate 504 / 429 / latency | โ (only by accident) | โ | โ mock_seq/mock_ratelimit/mock_delay |
| Right for | production reads | production writes (lists) | dev, CI, demos, workshops |
The pattern that uses each where it wins: base URL in an env var โ Jikan in production, your /m/YOUR_ID in .env.test. Your dev loop stops depending on MyAnimeList's mood. Related: Open Library API, PokeAPI, Rick and Morty API, mock any third-party API.