npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

yasuo.js

v0.3.2

Published

The modern, fully-typed, zero-dependency Riot Games API client for TypeScript. The evolution of Twisted, with a proactive rate limiter, lazy relations and async pagination.

Readme

Yasuo.js

A modern, zero-dependency TypeScript client for the Riot Games APIcomplete coverage of League of Legends, Teamfight Tactics, VALORANT, Legends of Runeterra, Tournament and the Riot Account API.

Yasuo is the evolution of twisted. It keeps everything that made twisted pleasant — a single client, typed responses, rate-limit info attached to every result — and rebuilds it around a Supabase-style query builder, lazy relation-aware chaining, a pluggable cache, a leveled logger and async iterators, all with no runtime dependencies. It wraps 100% of the key-authenticated Riot API surface, and lets you sign each product with its own API key.

🎮 Live demo — www.yasuo.gg

A League of Legends summoner-profile site — profiles, ranked, match history, champion mastery and a public JSON API — built entirely on this library, running on Cloudflare Workers. Live proof yasuo works in a real app.

import { Yasuo, Region, RegionGroup } from 'yasuo.js'

const yasuo = new Yasuo(process.env.RIOT_API_KEY)

// Every call is a query you run with .execute(). The result IS the entity —
// it carries its own `.error` and `.http`, and never throws for an API failure:
const account = await yasuo.riot.account
  .byRiotId('Hide on bush', 'KR1', RegionGroup.ASIA)
  .execute()

// Walk relations off the entity — this fetches ONLY the match list, not the summoner:
const matches = await account.summoner(Region.KR).matchIds({ count: 5 }).execute()

console.log(matches)              // Collection<string> — ['KR_1234…', …]
console.log(matches.error)        // null on success, the ApiError on failure
console.log(matches.http.rateLimits.app)  // rate-limit budget travels with every result

📚 Full documentation: https://docs.yasuo.gg/


Why yasuo?

| | twisted | leaguejs | yasuo | | --- | --- | --- | --- | | Runtime dependencies | a few | 8 (request, bluebird, …) | zero | | TypeScript | typed | none shipped (plain JS) | first-class, fully typed | | Response shape | { response, rateLimits } envelope | raw DTO, rejects on API errors | the entity itself, carrying .error + .http — never throws for API errors | | Requests | eager method calls | eager promise methods | query builders.byPuuid(…) builds, .execute() runs | | Chaining / relations | manual (fetch summoner → fetch matches) | manual | account.summoner(r).matchIds().execute()only the final request runs | | Rate limiting | reactive (retry on 429) | proactive (SPREAD), built-in | reactive by default, opt-in proactive pacing under Riot's limits | | Transport | fixed | fixed (deprecated request) | pluggable HttpClient + stackable axios-style middleware (global & per-service) | | Pagination | manual page loops | manual | async iterators (for await), start from any page | | Caching | — | in-memory (node-cache) | pluggable in-memory / Redis cache | | Logging | — | — | leveled logger (debug/info/warn/error), env-driven | | Magic strings | some | strings ('euw1', …) | none — everything is an enum | | Riot API coverage | LoL · TFT · Account | LoL only | LoL · TFT · VALORANT · LoR · Tournament · Account — 100% of the key-auth surface | | API keys | one key | one key | one key, or one per product (Riot's recommendation) — routed automatically | | Module format | CJS | CJS | dual ESM + CJS, single-file |


Install

bun add yasuo.js
# or
npm install yasuo.js

yasuo targets Node 18+ / Bun / Deno and ships a single-file dual ESM + CJS build with complete type declarations.

Quick start

import { Yasuo, Region, RegionGroup } from 'yasuo.js'

// The key can be passed directly or read from the RIOT_API_KEY env var.
const yasuo = new Yasuo({ key: process.env.RIOT_API_KEY })

// Game-scoped namespaces mirror Riot's own product split. Each method returns a
// query; run it with .execute() and use the entity directly:
const summoner = await yasuo.lol.summoner.byPuuid(puuid, Region.KR).execute()
const entries  = await yasuo.lol.league.byPuuid(puuid, Region.KR).execute()
const match    = await yasuo.lol.match.get('KR_1234567890', RegionGroup.ASIA).execute()

const tftMatch = await yasuo.tft.match.get('KR_1234567890', RegionGroup.ASIA).execute()
const account  = await yasuo.riot.account.byRiotId('Faker', 'KR1', RegionGroup.ASIA).execute()

Core concepts

Namespaces

The client is organised by product, matching Riot's routing model:

  • yasuo.lol.* — Summoner, League, Champion Mastery, Champion, Match, Spectator, Status, Clash, Challenges, Tournament (+ Tournament Stub)
  • yasuo.tft.* — Summoner, League, Match, Spectator
  • yasuo.val.* — Content, Match, Console Match, Ranked, Status (routes by Shard)
  • yasuo.lor.* — Match, Ranked, Status
  • yasuo.riot.account — the shared Account API (game name / tag line → PUUID)
  • yasuo.dataDragon — Riot's static data CDN (no key, no rate limits)

Per-product API keys

Riot recommends registering a separate product — and key — per game. Pass a keys map and yasuo signs every request with the key for that request's product, automatically; distinct keys get independent rate-limit budgets. Anything omitted falls back to the shared key, then to environment variables.

const yasuo = new Yasuo({
  keys: {
    lol: process.env.RIOT_LOL_KEY,
    tft: process.env.RIOT_TFT_KEY,
    val: process.env.RIOT_VAL_KEY,
    lor: process.env.RIOT_LOR_KEY,
  },
  key: process.env.RIOT_API_KEY, // shared fallback (e.g. for the Account API)
})

await yasuo.val.content.get(Shard.NA).execute()   // signed with the VAL key
await yasuo.lol.summoner.byPuuid(puuid, Region.KR).execute() // signed with the LoL key

Resolution order per request: keys[game]RIOT_<GAME>_API_KEY env → shared keyRIOT_API_KEY env. The Account API borrows any configured product key when it has none of its own. A single key (or new Yasuo(process.env.RIOT_API_KEY)) still works exactly as before.

VALORANT, Legends of Runeterra & Tournaments

import { Yasuo, Shard, RegionGroup, ValQueue } from 'yasuo.js'

// VALORANT routes by Shard; a matchlist is relation-aware.
const list  = await yasuo.val.match.matchlist(puuid, Shard.NA).execute()
const match = await list.match(list.matchIds()[0]).execute()
const board = await yasuo.val.ranked.leaderboard(actId, Shard.NA, { size: 10 }).execute()

// Legends of Runeterra routes by RegionGroup.
const lorMatches = await yasuo.lor.match.byPuuid(puuid, RegionGroup.AMERICAS).execute()

// Tournament-V5 (POST flow) — or `tournamentStub` with no production key:
const provider = await yasuo.lol.tournamentStub
  .registerProvider({ region: 'NA', url: 'https://cb.example' }, RegionGroup.AMERICAS)
  .execute()

Query builders + the result model

Every method returns a query builder — nothing hits the network until you call the terminal .execute(). It resolves to the entity (or collection) directly, and that entity carries its own error and HTTP context. A request never throws for an API-level failure: on failure the DTO fields are absent, .error holds the original rich ApiError and .http.ok is false; on success .error is null.

const summoner = await yasuo.lol.summoner.byPuuid(puuid, Region.KR).execute()

summoner.summonerLevel   // typed DTO field, right on the entity
summoner.error           // ApiError | null   (the original failure)
summoner.http.ok         // boolean
summoner.http.status     // 200
summoner.http.rateLimits.app  // [{ limit, intervalSeconds, count }]
summoner.http.headers    // raw, lower-cased response headers

// Idiomatic: branch on `.error`.
if (summoner.error) return console.error(summoner.error.status, summoner.error.body)
console.log(summoner.summonerLevel)

// Prefer exceptions? Opt in per call:
const strict = await yasuo.lol.summoner.byPuuid(puuid, Region.KR).execute({ throw: true })

// Want exactly what Riot returned? Pass { raw: true } — typed `unknown` by
// default, or supply the shape you expect as a type argument:
const raw = await yasuo.lol.summoner.byPuuid(puuid, Region.KR).execute({ raw: true })
const dto = await yasuo.lol.summoner
  .byPuuid(puuid, Region.KR)
  .execute<{ puuid: string; summonerLevel: number }>({ raw: true }) // typed, no cast

Scalar endpoints (e.g. a mastery score) can't hang metadata off a primitive, so they resolve a small ValueResult<T> — read the number from .value, with the same .error/.http:

const score = await yasuo.lol.mastery.score(puuid, Region.KR).execute()
console.log(score.value)   // number | null

Lazy relations — the key idea

byPuuid(...) returns a lazy reference that is itself a query (.execute() fetches the summoner). Calling a relation returns another query that fetches only that related resource — the summoner request is skipped entirely:

// One request (the match list) — the summoner is never fetched:
const ids = await yasuo.lol.summoner.byPuuid(puuid, Region.KR).matchIds({ count: 20 }).execute()

// Running the ref itself fetches the summoner:
const summoner = await yasuo.lol.summoner.byPuuid(puuid, Region.KR).execute()

Relations know how to route themselves. A summoner on Region.KR traverses to their match history on RegionGroup.ASIA automatically — you never re-specify the routing:

const account = await yasuo.riot.account.byRiotId('Hide on bush', 'KR1', RegionGroup.ASIA).execute()
const ref = account.summoner(Region.KR)

const masteries = await ref.topChampionMasteries(5).execute()
const live      = await ref.activeGame().execute()   // CurrentGameEntity | null
const match     = await yasuo.lol.match.get(ids[0], RegionGroup.ASIA).execute()
const timeline  = await match.timeline().execute()   // ids reused, region derived

No magic strings

Regions, queues, tiers, divisions, match types, challenge levels, HTTP details — all enums:

import { Region, RegionGroup, RankedQueue, Tier, Division, MatchType } from 'yasuo.js'

// Async iterators throw on failure, which is idiomatic for `for await`:
for await (const entry of yasuo.lol.league.streamEntries(
  RankedQueue.SOLO_5x5, Tier.DIAMOND, Division.I, Region.EUW, { startPage: 3 },
)) {
  console.log(entry.puuid, entry.leaguePoints)
}

Rate limiting

Reactive retries are always on: a 429/503 honours retry-after with bounded backoff. Proactive pacing is opt-in (rateLimit: true) — enable it and the limiter reads Riot's x-app-rate-limit / x-method-rate-limit headers and paces requests underneath the advertised limits using sliding windows, per application host and per method, so you avoid 429s before they happen.

const yasuo = new Yasuo({
  key,
  rateLimit: true,          // opt in to proactive pacing (off by default)
  retry: { maxAttempts: 3 } // reactive retry policy (on by default)
})

See the rate-limiting guide.

Caching

Opt-in response caching, served before the rate limiter is even consulted. In-memory by default; bring your own store — Redis or Cloudflare KV need no dependency (yasuo just wants a client that matches RedisClientLike / KVNamespaceLike), or implement the CacheStore interface for anything else:

import { Yasuo, RedisCache, KVCache } from 'yasuo.js'

const yasuo = new Yasuo({ key, cache: true })                        // in-memory, 60s TTL
const y2    = new Yasuo({ key, cache: { store: new RedisCache(redis), ttlMs: 30_000 } })
const y3    = new Yasuo({ key, cache: { store: new KVCache(env.RIOT_CACHE) } })  // Cloudflare Workers KV

// Or pass a raw client/namespace and yasuo wraps it (redis-like -> RedisCache, kv-like -> KVCache):
const y4    = new Yasuo({ key, cache: { store: redis } })
const y5    = new Yasuo({ key, cache: { store: env.RIOT_CACHE } })

See the caching guide.

Custom transport & middleware

Inject any HttpClient (a single send(request) method) — a proxy, an undici pool, a mock in tests — and stack axios-style middleware. Middleware runs in an onion: global middleware (yasuo.use(...) or the middleware config) wraps per-service middleware (yasuo.lol.summoner.use(...)), which wraps the transport. Each can inspect or rewrite the request and response, short-circuit, or retry.

import { Yasuo, type HttpMiddleware } from 'yasuo.js'

const timing: HttpMiddleware = async (request, next, { endpointId }) => {
  const started = performance.now()
  const response = await next(request)
  console.log(`${endpointId} → ${response.status} in ${(performance.now() - started).toFixed(0)}ms`)
  return response
}

const yasuo = new Yasuo({ key, httpClient: myClient, middleware: [timing] })

// Add more at runtime — global, or scoped to one service:
yasuo.use((request, next) => next({ ...request, headers: { ...request.headers, 'x-app': 'my-bot' } }))
yasuo.lol.match.use((request, next) => { console.debug('match', request.url); return next(request) })

See the transport & middleware guide.

Logging

A leveled logger driven by config or the YASUO_LOG_LEVEL / LOG_LEVEL env var (debug · info · warn · error · silent):

import { Yasuo, LogLevel } from 'yasuo.js'

const yasuo = new Yasuo({ key, logLevel: LogLevel.DEBUG })  // or set YASUO_LOG_LEVEL=debug

See the logging guide.

Pagination & async iterators

Paginated endpoints return a Paginator you can iterate with for await, collect with .toArray(), peek with .first(), or walk page-by-page with .pages() — starting from any page/offset. Iterators throw on failure (they use { throw: true } internally), which is the natural contract for for await:

for await (const id of yasuo.lol.match.streamIds(puuid, RegionGroup.ASIA, { start: 0, pageSize: 100 })) {
  // …every match in a player's history, paced by the rate limiter
}

// Or collect a bounded slice eagerly:
const firstFifty = await yasuo.lol.match.streamIds(puuid, RegionGroup.ASIA).toArray(50)

See the pagination guide.

Error handling

A request never throws for an API error — inspect the entity's .error, a typed subclass of ApiError (itself a YasuoError) that carries the original HTTP response:

import { NotFoundError, RateLimitError, ForbiddenError } from 'yasuo.js'

const summoner = await yasuo.lol.summoner.byPuuid(puuid, Region.KR).execute()

if (summoner.error instanceof NotFoundError)       { /* 404 */ }
else if (summoner.error instanceof RateLimitError) { /* 429 — error.rateLimits.retryAfterSeconds */ }
else                                               { console.log(summoner.summonerLevel) }

Prefer exceptions? .execute({ throw: true }) throws that same ApiError instead of attaching it. Misuse (a missing/invalid key) always throws, regardless.

See the error-handling guide.

API coverage

100% of the key-authenticated Riot Games API — every product, every endpoint a Riot API key can call. 74 endpoints:

  • LoL (44): Summoner · League (+ league-exp) · Champion Mastery · Champion Rotation · Match-V5 · Spectator · Status · Clash · Challenges · Tournament-V5 · Tournament-Stub-V5
  • TFT (14): Summoner · League (+ rated ladder) · Match · Spectator
  • VALORANT (8): Content · Match · Console Match · Ranked · Status
  • Legends of Runeterra (4): Match · Ranked · Status
  • Riot (4): Account (by PUUID / by Riot ID) · Active Shard · Active Region
  • Data Dragon: versions, champions, runes, static reference data

The only Riot endpoints not wrapped are the RSO/OAuth-gated ones (LoR Deck & Inventory, RSO Match), which require a user-authorization flow rather than an API key.

See the endpoint map for the full list.

Documentation

Full docs live at https://docs.yasuo.gg/:

Showcase

  • yasuo.gg — a complete League of Legends summoner-profile site and public JSON API, built entirely on yasuo and running on Cloudflare Workers. A real, end-to-end demonstration that the library works.

Migrating from twisted

yasuo is a spiritual successor, not a drop-in replacement — the ergonomics are better. The short version: constructor takes a config object, LolApi/TftApi/RiotApi become yasuo.lol/yasuo.tft/yasuo.riot, the { response, rateLimits } envelope becomes the entity itself (carrying .error/.http) that you run with .execute(), and encrypted-summoner-id lookups give way to PUUIDs. Full mapping in the migration guide.

Development

bun install
bun run typecheck     # tsc --noEmit
bun run lint          # biome check .
bun run test:unit     # unit tests (no network) — coverage-gated at 95%
bun run test:integration  # live tests (needs RIOT_API_KEY)
bun run build         # single-file ESM + CJS + d.ts
bun run docs:serve    # preview the MkDocs site locally

Unit tests run network-free (inject MockHttpClient or a fake HttpClient/fetch) and are coverage-gated at 95% line/statement coverage (currently ~98%) via bunfig.toml. Conventions for contributors (folder layout, one-declaration-per-file, enum rules) live in the architecture guide.

License

MIT © justadev-afk