Recipes
Complete, copy-pasteable workflows. Each recipe states its provider scope.
AniList: paginated list sync
Provider: AniList. Collect a user's anime list across all pages and upsert 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(21, {
fields: ["id", "title", "main_picture", "synopsis", "mean"],
});
console.log(anime.title, anime.main_picture?.medium);id and title are always present. Other fields appear when requested. See MAL operations.
Cross-provider title comparison
Providers: both. Compare how the two databases title the same show. AniLink does not normalize data across providers — you map between them yourself.
import { AniLink } from "anilink-api-wrapper";
const aniLink = new AniLink({
anilist: { authToken: process.env.ANILIST_TOKEN },
mal: { accessToken: process.env.MAL_TOKEN },
});
const [anilistMedia, malAnime] = await Promise.all([
aniLink.anilist.query.media({ id: 21, type: "ANIME" }),
aniLink.mal.anime.get(21, { fields: ["id", "title"] }),
]);
console.log("AniList:", anilistMedia.media?.title?.romaji);
console.log("MAL:", malAnime.title);Background token-refresh loop
Provider: MAL. Refresh before expiry. 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 equivalent uses getTokenExpiry and refreshAccessToken with the same proactive pattern. See MAL authentication and AniList authentication.
Resilient scheduler
Providers: both. Pace requests, fail fast during outages, and observe latency.
import { AniLink } from "anilink-api-wrapper";
const aniLink = new AniLink({
anilist: {
authToken: process.env.ANILIST_TOKEN,
paceWithRateLimit: true,
circuitBreaker: { threshold: 5, cooldownMs: 30_000 },
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 — when a recipe misbehaves.
- Operation reference — the operations used above in full detail.