Harshit
Building crunchy-api: A Modern TypeScript Client for Crunchyroll
Observation

Building crunchy-api: A Modern TypeScript Client for Crunchyroll

Apr 19, 2026
3 min read
Harshit

I love anime—Tokyo Ghoul, Attack on Titan, Fullmetal Alchemist: Brotherhood. As a developer, naturally, I wanted to pull my watch history and currently watching lists into my own apps. The problem? Crunchyroll doesn't have a public, documented API for developers to easily play with.

So, I built one.

Enter crunchy-api

I engineered crunchy-api, a fully-typed TypeScript client that seamlessly interacts with Crunchyroll's internal API. It handles token generation, session management, and parses enormous JSON payloads into strict, predictable TypeScript interfaces.

Here’s how you can use it to pull your own data.

1. The Core Client

The foundation of the package is the CrunchyrollClient. It's designed to run beautifully in Node.js, edge runtimes, or Next.js React Server Components.

Code
import { CrunchyrollClient } from "crunchy-api";
 
const client = new CrunchyrollClient({
  locale: "en-US",
  timeoutMs: 15000,
});
Tap to expand

2. Authentication Contexts

Crunchyroll has a few ways to authenticate. For public data (like fetching season episodes), you can just grab an anonymous token:

Code
// 1. Get an anonymous token for public catalog browsing
const anonToken = await client.getAnonymousToken();
console.log("Token:", anonToken.access_token);
Tap to expand

To access your personal Watch History or custom Crunchylists, you need to use your refresh token cookie (etp_rt).

Code
// 2. Authenticate using your browser cookie
const session = await client.authenticateWithRtCookie({
  cookie: process.env.CRUNCHYROLL_COOKIE,
  deviceType: "Chrome on Android"
});
Tap to expand

The client handles the heavy lifting of hitting the underlying OAuth endpoints, resolving profiles, and managing the active session automatically.

3. Pulling Your Anime Data

Once authenticated, fetching data is as simple as calling a typed method. Under the hood, I've mapped out complex objects (like CrunchyItem, EpisodeInfo, and WatchHistoryEntry) so you get perfect intellisense and avoid parsing errors.

Code
// Fetch what you are currently watching
const currentlyWatching = await client.getCurrentlyWatching({
  page_size: 10
});
 
// Or fetch your custom Crunchylists
const lists = await client.getCrunchylists();
 
// Get specific list items
if (lists.data.length > 0) {
  const items = await client.getCrunchylistItems(lists.data[0].id);
  console.log(items.data);
}
Tap to expand

Wait, before we even authenticate, you can totally fetch public catalog data globally.

Let's fetch season episodes of Wistoria S2 anonymously, directly pulling info using client.getSeasonEpisodes(seasonId) inside this Next.js server component:

Live Data / Season Episodes

That Time I Got Reincarnated as a Slime (S4)

New Days

1. New Days

EP

Tempest is thriving after the success of its festival, but unexpected trouble has arisen with the new dungeon.

The Dungeon Evolves

2. The Dungeon Evolves

EP

Rimuru and his trusted companions discuss ideas for improving the dungeon.

The Avatar Team Is Formed

3. The Avatar Team Is Formed

EP

Rimuru, Ramiris, Veldora, and Milim work on perfecting their avatars before facing Team Green Fury.

Invitation

4. Invitation

EP

The Council of the West deliberates on whether to grant membership to Tempest.

The First Step

5. The First Step

EP

Rimuru attends the meeting that will determine Tempest's membership in the Council of the West.

The Council of the West

6. The Council of the West

EP

A list of unreasonable demands from the Council makes Rimuru realize that a deeper plot is in motion.

The Mastermind's Identity

7. The Mastermind's Identity

EP

An interrogation of Prince Elric's would-be shooter reveals who's pulling the strings behind the Council.

The Groundwork of Greed

8. The Groundwork of Greed

EP

Glenda reveals all she knows about the Council and the Rozzos, and Rimuru prepares to inspect ancient ruins.

Investigating the Ruins

9. Investigating the Ruins

EP

The investigation of the ancient ruins of Amrita begins.

The Master of Greed

10. The Master of Greed

EP

Rimuru and Mirabel come face-to-face at last in the ancient ruins.

Milim's Friend

11. Milim's Friend

EP

Rimuru helps Milim confront the Chaos Dragon, a corrupted form of the baby dragon that was once her friend.

Tempest Evolves

12. Tempest Evolves

EP

Rimuru shares his plans for Tempest's future, including a new system of government and Council representation.

New Companions

13. New Companions

EP

Ramiris and Veldora are feeling overworked, so they ask Rimuru to find assistants for them.

The Black Numbers

14. The Black Numbers

EP

Diablo returns with new demon subordinates to add to Tempest's ranks.

An Unsettling Feeling

15. An Unsettling Feeling

EP

As Tempest continues to grow and evolve, the Moderate Harlequin Alliance approaches Demon Lord Leon.

You can also snag a dedicated episode instance, leveraging client.getEpisodeInfo(episodeId) just like this:

An Unsettling Feeling
That Time I Got Reincarnated as a SlimeS4 E15

An Unsettling Feeling

As Tempest continues to grow and evolve, the Moderate Harlequin Alliance approaches Demon Lord Leon.

And here is another live demo connecting securely and dumping my personal Crunchylist dynamically inside this blog content!

Live Data / Crunchylists

Best Action

Currently has 1 saved shows.

Building a Next.js Integration Harness

To prove out the package, I built a Next.js integration harness that runs on the server and invokes each package method. This ensures that features like force-dynamic API routes and React Server Components play nicely with the crunchy-api fetch implementations.

Because the returned data can be nested (e.g., episode info is sometimes wrapped in a panel object, and image arrays are deeply nested), I built a robust normalizer component:

Code
function getImageUrl(images: any): string | null {
  const sources = images.thumbnail || images.poster_wide || images.poster_tall;
  if (Array.isArray(sources) && sources.length > 0 && Array.isArray(sources[0])) {
    const list = sources[0];
    const item = list.length > 3 ? list[3] : list[list.length - 1];
    return item?.source || null;
  }
  return null;
}
Tap to expand

If you're building an anime tracker, Discord bot, or a Next.js portfolio widget that brags about what you’re currently watching, crunchy-api handles all the session management and TypeScript generics for you out of the box.

Here is a live demo pulling my actual watch history right now, rendered natively in MDX using Server Components:

Grab the package from NPM or check out the source on my GitHub. Now, back to watching AOT.