Skip to content
墨 AniLink

Recipes ​

Each recipe states its provider scope and includes a copy-pasteable example.

AniList: paginated list sync ​

Provider: AniList. Fetch every page of a user's anime list and upsert it into a local store.

typescript
import { AniLink } from "anilink-api-wrapper";

const aniLink = new AniLink(process.env.ANILIST_TOKEN);

const result = await aniLink.anilist.paginate(
    (page, perPage) =>
        aniLink.anilist.query.page.medias({
            page,
            perPage,
            type: "ANIME",
            sort: ["TRENDING_DESC"],
        }),
    "media",
    { perPage: 50, maxPages: 10, concurrency: 4 }
);

console.log(`synced ${result.items.length} items across ${result.pageCount} pages`);
if (result.truncated) console.warn("stopped at maxPages before the source ran out");

paginate keeps results in page order even with concurrency > 1. See Pagination.

MAL: anime lookup with selected fields ​

Provider: MAL. Fetch only the fields you need.

typescript
import { AniLink } from "anilink-api-wrapper";

const aniLink = new AniLink({ mal: { accessToken: process.env.MAL_TOKEN } });

const anime = await aniLink.mal.anime.get(
    { id: 21 },
    { fields: ["id", "title", "main_picture", "synopsis", "mean"] }
);

console.log(anime.title, anime.main_picture?.medium);

The id and title fields are always present. Other fields appear when you request them. See MAL operations.

Cross-provider title comparison ​

Providers: both. Compare the titles AniList and MAL return for the same show. AniList media includes idMal, the MyAnimeList ID for that entry. The crossLink helper builds ID maps from AniList results. AniLink does not normalize data across providers, so titles, scores, and statuses remain provider-specific.

typescript
import { AniLink } from "anilink-api-wrapper";

const aniLink = new AniLink({
    anilist: { authToken: process.env.ANILIST_TOKEN },
    mal: { accessToken: process.env.MAL_TOKEN },
});

const anilistMedia = await aniLink.anilist.query.media({ id: 21, type: "ANIME" });

// Build the id lookup maps from the AniList result.
const { anilistToMal } = aniLink.anilist.crossLink([anilistMedia]);

const malId = anilistToMal.get(21);
if (malId !== undefined) {
    const malAnime = await aniLink.mal.anime.get({ id: malId }, { fields: ["id", "title"] });
    console.log("AniList:", anilistMedia.title.romaji);
    console.log("MAL:", malAnime.title);
}

To process a full page, pass page.media directly to crossLink. The helper makes no requests. It returns entries without a MAL ID in unmapped:

typescript
const page = await aniLink.anilist.query.page.medias({ page: 1, perPage: 50, type: "ANIME" });
const { anilistToMal, unmapped } = aniLink.anilist.crossLink(page.media);

const malIds = page.media
    .map((media) => anilistToMal.get(media.id))
    .filter((malId): malId is number => malId !== undefined);

console.log(`${malIds.length} mapped, ${unmapped.length} without a MAL id`);

Background token-refresh loop ​

Provider: MAL. Refresh before expiry, not after the 401. Keep the stored refresh token when MAL does not rotate it.

typescript
import {
    getMalTokenExpiry,
    refreshMalAccessToken,
    type MalTokenResponse,
} from "anilink-api-wrapper";

let token: MalTokenResponse = /* stored from the initial exchange */ {} as MalTokenResponse;

async function ensureFreshToken(): Promise<MalTokenResponse> {
    if (Date.now() < getMalTokenExpiry(token).getTime() - 60_000) return token;
    const refreshed = await refreshMalAccessToken({
        clientId: process.env.MAL_CLIENT_ID!,
        refreshToken: token.refresh_token!,
    });
    token = { ...refreshed, refresh_token: refreshed.refresh_token ?? token.refresh_token };
    return token;
}

The AniList helpers getTokenExpiry and refreshAccessToken work the same way. See MAL authentication and AniList authentication.

Resilient scheduler ​

Providers: both. This example paces AniList requests, enables its circuit breaker, retries MAL requests, and records response times from both providers.

typescript
import { AniLink } from "anilink-api-wrapper";

const aniLink = new AniLink({
    anilist: {
        authToken: process.env.ANILIST_TOKEN,
        paceWithRateLimit: true,
        circuitBreaker: { threshold: 5, cooldownMs: 30_000 },
        onCircuitOpen: ({ host, failures }) =>
            metrics.increment("circuit.open", { host, failures }),
        onCircuitClose: ({ host }) => metrics.increment("circuit.close", { host }),
        onResponse: ({ durationMs }) => metrics.record("anilist", durationMs),
    },
    mal: {
        accessToken: process.env.MAL_TOKEN,
        retry: { maxRetries: 2 },
        onResponse: ({ durationMs }) => metrics.record("mal", durationMs),
    },
});

Next steps ​

AniLink, typed AniList & MyAnimeList client for TypeScript.