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.
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.
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.
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:
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.
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.
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
- Troubleshooting & FAQ for when a recipe does not work as expected.
- Operation reference documents the operations used above.