Skip to content

denispianelli/chesscom

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

45 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

chesscom-sdk

npm version license types

Unofficial TypeScript SDK for the Chess.com Published-Data API. Not affiliated with, endorsed by, or sponsored by Chess.com.

A clean, typed, isomorphic client for the public Chess.com API — with built-in rate limiting, ETag caching, runtime response validation, and lazy pagination.

  • ✅ Typed responses, validated at runtime with zod
  • ✅ Serial rate limiting + backoff (respects the Chess.com "be serial" rule)
  • ✅ Transparent ETag caching (304 Not Modified aware)
  • ✅ Lazy async iteration over monthly game archives
  • ✅ Isomorphic — native fetch, runs in Node 22+, Deno, Bun, the browser
  • ✅ One dependency (zod)

✅ Stable since 1.0.0. The public API follows semver — breaking changes ship only in a new major version.

Install

npm install chesscom-sdk

Quickstart

import { ChessComClient } from "chesscom-sdk";

const client = new ChessComClient({
  // Required by Chess.com — include an app name and a contact.
  userAgent: "myapp/1.0 (me@example.com)",
});

const profile = await client.getPlayer("hikaru");
const stats = await client.getPlayerStats("hikaru");

// Lazily stream a player's games across monthly archives:
for await (const game of client.streamPlayerGames("hikaru", {
  since: "2024-01",
})) {
  console.log(game.url, game.pgn);
}

Why a userAgent is required

Chess.com rejects requests without a descriptive User-Agent (HTTP 403) and asks that you include a way to contact you. The client therefore requires it at construction. Use "<app>/<version> (<contact>)", e.g. "my-bot/1.0 (me@example.com)".

Note: in browsers, fetch ignores a custom User-Agent (it is a forbidden header). The option only takes effect on Node, Deno, and Bun.

API

All methods return validated, fully typed results.

Method Returns Endpoint
getPlayer(username) PlayerProfile /player/{username}
getPlayerStats(username) PlayerStats /player/{username}/stats
getPlayerArchives(username) string[] /player/{username}/games/archives
getPlayerGames(username, year, month) Game[] /player/{username}/games/{YYYY}/{MM}
getPlayerGamesPgn(user, year, month) string (raw PGN) /player/{username}/games/{YYYY}/{MM}/pgn
streamPlayerGames(username, options?) AsyncGenerator<Game> iterates over the monthly archives
getPlayerGamesToMove(username) ToMoveGame[] /player/{username}/games/to-move
getPlayerClubs(username) PlayerClub[] /player/{username}/clubs
getPlayerTournaments(username) PlayerTournaments /player/{username}/tournaments
getClub(urlId) ClubProfile /club/{url-id}
getClubMembers(urlId) ClubMembers /club/{url-id}/members
getTournament(urlId) Tournament /tournament/{url-id}
getTournamentRound(urlId, round) TournamentRound /tournament/{url-id}/{round}
getTournamentRoundGroup(urlId, r, g) TournamentRoundGroup /tournament/{url-id}/{round}/{group}
getLeaderboards() Leaderboards /leaderboards
getStreamers() Streamer[] /streamers
getDailyPuzzle() Puzzle /puzzle
getRandomPuzzle() Puzzle /puzzle/random
getCountry(iso) Country /country/{iso}
getCountryPlayers(iso) string[] /country/{iso}/players
getCountryClubs(iso) string[] /country/{iso}/clubs
getPlayerMatches(username) PlayerMatches /player/{username}/matches
getClubMatches(urlId) ClubMatches /club/{url-id}/matches
getMatch(id) Match /match/{id}
getMatchBoard(id, board) MatchBoard /match/{id}/{board}
getTitledPlayers(title) string[] /titled/{title}

Each method also accepts a final options object with an AbortSignal:

const controller = new AbortController();
const profile = await client.getPlayer("hikaru", { signal: controller.signal });

Return conventions

The API wraps most collections in an envelope. Two rules keep returns predictable:

  • Single-key array envelopes are unwrapped. Endpoints whose payload is just { games: [...] }, { players: [...] }, { clubs: [...] }, etc. return the array directly (Game[], string[], …) — the wrapper carries no extra meaning.
  • Multi-key structures are returned as-is. When the grouping is the data — { finished, in_progress, registered } (matches, tournaments), { weekly, monthly, all_time } (club members), the leaderboard categories — the object is returned whole (PlayerMatches, ClubMembers, …).

Numeric path parameters

year/month are typed number (they are validated — month must be 1–12). Opaque path ids — a match id, a board, a tournament round/group — accept number | string, since they are passed through verbatim and can exceed Number.MAX_SAFE_INTEGER.

streamPlayerGames

Hides the monthly pagination: it lists the archives, then fetches one month at a time (lazily) and yields game by game. The rate limiter and cache apply per month, so re-runs are fast and polite.

for await (const game of client.streamPlayerGames("hikaru", {
  since: "2024-01", // YYYY-MM, inclusive
  until: "2024-12", // YYYY-MM, inclusive
  order: "newest-first", // or "oldest-first" (default: newest-first)
  timeClass: "blitz", // keep only blitz games
  rated: true, // keep only rated games
})) {
  // …
}

Months outside the since/until window are never requested.

Parsing PGN

Games expose their raw PGN as a string — this SDK does not parse moves, so you can pair it with whatever chess library you prefer. For example, with chess.js:

import { Chess } from "chess.js";

const games = await client.getPlayerGames("hikaru", 2024, 1);
const game = games[0];

if (game?.pgn) {
  const chess = new Chess();
  chess.loadPgn(game.pgn);
  console.log(chess.history()); // ["e4", "c5", "Nf3", …]
  console.log(chess.header()); // { White, Black, Result, ECO, … }
}

(Header fields like white, black, time_control, eco, and end_time are also available as structured fields on the Game object, no parsing needed.)

Configuration

new ChessComClient({
  userAgent: "myapp/1.0 (me@example.com)", // required
  fetch, // custom fetch (default: global fetch)
  cache, // custom CacheStore (default: in-memory Map)
  timeout: 10_000, // per-request timeout in ms (default: none)
  baseUrl: "https://api.chess.com/pub", // default
  onValidationError: "throw", // "throw" | "warn" | "ignore" (default: "throw")
  onRateLimit: (info) => console.warn("rate limited", info),
});

Error handling

Every error thrown by the SDK extends ChessComError and carries a discriminant kind. Branch with instanceof or switch (err.kind).

import { ChessComError, NotFoundError } from "chesscom-sdk";

try {
  await client.getPlayer("does-not-exist");
} catch (err) {
  if (err instanceof NotFoundError) {
    // …
  } else if (err instanceof ChessComError) {
    console.error(err.kind, err.status, err.url);
  }
}
Error kind When
NotFoundError not_found HTTP 404 / 410
RateLimitError rate_limit HTTP 429 after retries are exhausted
ForbiddenError forbidden HTTP 403 (often a missing/rejected User-Agent)
ServerError server HTTP 5xx or an unexpected status
ValidationError validation A response did not match its schema
NetworkError network The request never produced a response

Validation

Responses are validated against zod schemas. If the API drifts from the expected shape, onValidationError decides what happens:

  • "throw" (default) — throw a ValidationError.
  • "warn" — log a warning and return the raw data.
  • "ignore" — return the raw data silently.

Rate limiting

Chess.com asks clients to make requests serially (parallel requests get a 429). By default the client funnels all requests through a serial queue and retries a 429 with exponential backoff, honoring the server's Retry-After. This is per-client instance; share one client to share the queue.

Caching

The client revalidates with ETags (If-None-Match) and serves the cached body on 304 Not Modified. The default store is an in-memory Map. Plug in your own by implementing CacheStore:

import type { CacheStore, CacheEntry } from "chesscom-sdk";

class RedisCacheStore implements CacheStore {
  constructor(private redis: import("ioredis").Redis) {}

  async get(key: string): Promise<CacheEntry | undefined> {
    const raw = await this.redis.get(key);
    return raw ? (JSON.parse(raw) as CacheEntry) : undefined;
  }

  async set(key: string, value: CacheEntry): Promise<void> {
    await this.redis.set(key, JSON.stringify(value));
  }
}

const client = new ChessComClient({
  userAgent: "myapp/1.0 (me@example.com)",
  cache: new RedisCacheStore(redis),
});

Requirements

  • The published library runs on Node 22+, Deno, Bun, and browsers (anything with a global fetch).
  • Contributing to this repo requires Node 22+ (the dev toolchain).

Contributing

Contributions are welcome — see CONTRIBUTING.md.

Documentation

License

MIT