bgg-ts-client
v0.3.0
Published
Fully typed TypeScript client for the BoardGameGeek XMLv2 API with error handling and pagination support
Downloads
767
Maintainers
Readme
bgg-ts-client
A TypeScript client for the official BoardGameGeek XML API v2.
Note: This project is a fork of boardgamegeekclient by learningprocesss. It is now developed and maintained independently.
Key features
- :ballot_box_with_check: Support Authorization via BGG tokens
- :ballot_box_with_check: Fully typed requests and responses
- :ballot_box_with_check: Easy to use
- :ballot_box_with_check: Typescript written
- :ballot_box_with_check: Promisified
- :ballot_box_with_check: thing, family, forumlist, forum, thread, user, guild, play, collection, search, hot endpoints
- :ballot_box_with_check: Structured error handling with typed error classes

Prerequisites
Starting in Fall 2025, BoardGameGeek requires all API clients to use authorization. Before using this package, you must register your application on BoardGameGeek and obtain an access token.
Register and manage your application here: https://boardgamegeek.com/applications
Installation
npm i bgg-ts-clientyarn add bgg-ts-clientBreaking changes (migrating from 0.2 → 0.3)
0.3 is a major internal rewrite. The high-level BggClient API (.query() /
.queryWithProgress() per endpoint) is unchanged, but imports, DTO type names, and
several field shapes changed. Full detail with before/after tables is in
CHANGELOG.md. Everything that can break existing code:
Imports & exports
Per-item DTOs are now exported from the package root. In 0.2 the root only exported
BggClientand the error classes, so consumers deep-imported types frombgg-ts-client/dist/esm/dto/concrete/subdto/…. Those internal folders (dto/concrete/subdto/anddto/concrete/paginated/) have been removed — deep imports into them break. Import from the root instead:// 0.2 import { BggCollectionItemDto } from 'bgg-ts-client/dist/esm/dto/concrete/subdto/BggCollectionItemDto'; // 0.3 import { BggCollectionItemDto, BggThingDto } from 'bgg-ts-client';Removed standalone types:
BggArticleDto,BggForumlistForumDto,BggForumThreadDto(standalone file),BggGuildMemberDto/BggGuildMemeberDto,BggThingMarketlistingsDto,BggStatisticsPaginatedDto,BggStatisticsRatingDto,BggStatisticsRatingRanksDto,BggThingVideoPaginatedDto,BggThingCommentPaginatedDto,BggPollResultDto,BggPollResultItemDto, and theBggPlaysPlay*family. Their data now lives inline on the endpoint DTOs listed below.IDtoParserno longer exposesjsonToDtoor aparserfield — use the synchronousparse(parsedXml).Dependencies:
jackson-jsremoved;fast-xml-parserbumped^3.18→^4.5.
Type renames
| 0.2 | 0.3 |
|---|---|
| BggCollectionItemStatusDto | BggCollectionStatusDto |
| BggCollectionItemStatsDto | BggCollectionStatsDto |
| BggPlaysPlayDto | BggPlayDto (⚠ meaning changed — see below) |
| BggPlaysPlayPlayerDto | BggPlayPlayerDto |
| BggForumlistForumDto | BggForumDescriptorDto |
| BggThingMarketlistingsDto | BggMarketplaceItemDto |
| BggGuildMemeberDto (typo) | nested inside BggGuildDto |
BggPlayDto meaning changed
In 0.2, BggPlayDto was the wrapper returned by client.play.query()
(username, userid, total, page, plays). In 0.3 that wrapper is the new
BggPlaysDto, and BggPlayDto is a single play. Re-type references that touch
.plays / .username as BggPlaysDto.
BggThingDto field changes
name: string(single) is removed — usenames: BggNameDto[](each withtype: 'primary' | 'alternate',sortindex,value).alternateNames: string[]is retained and derived fromnames.statisticsis flattened: wasBggStatisticsPaginatedDtowith a nested.ratingsobject (statistics.ratings.average,statistics.ratings.averageweight,statistics.ratings.ranks). Nowstatistics: BggThingStatsDto | undefinedwith those fields directly on it —statistics.average,statistics.averageweight,statistics.ranks. The.ratingslayer is gone.marketplacelistings: BggThingMarketlistingsDto[]renamed tomarketplace: BggMarketplaceItemDto[] | undefined.videoswas a paginated wrapper (BggThingVideoPaginatedDto) → nowvideos: BggVideoDto[] | undefined.comments: BggThingCommentPaginatedDto→comments: BggThingCommentsDto | undefined(same{ totalitems, page, items }shape).pollsrestructured:BggPollDto[]is now a discriminated union onname(suggested_numplayers|suggested_playerage|language_dependence),totalvotesis anumber, andresultsis typed per poll.resultItemListis removed — readresultsdirectly.pollSummary: any→pollSummary: BggPollSummaryDto[] | undefined.- Previously-required scalars (
type,description,yearpublished, …) are now| undefined; guard them understrictNullChecks.
Field type coercions (strings/flags → real types)
BGG's "0"/"1" flags, "yes"/"no", dates, and numeric strings are now parsed into
boolean, Date, and number. Notable cases:
- Collection —
BggCollectionStatusDtoflags (own,prevowned,fortrade,want,wanttoplay,wanttobuy,wishlist,preordered)"0"/"1"→boolean | undefined;lastmodifiedstring→Date | undefined. NewnameSortindexfield. - Plays —
BggPlayDto.date→Date | undefined;incomplete,nowinstats→boolean | undefined.BggPlayPlayerDto:new/win→boolean | undefined,rating→number | undefined,score→string | undefined. - Dates elsewhere —
BggForumDescriptorDto.lastpostdate,BggForumDto.lastpostdate,BggForumThreadDto.postdate/lastpostdate,BggGuildDto.created,BggUserDto.lastlogin,BggVideoDto.postdate,BggMarketplaceItemDto.listdate→Date | undefined. The BGG "never" sentinel (Thu, 01 Jan 1970 …) is preserved asnew Date(0), not normalized away. BggLinkDto.inbound: boolean | undefinedis now exposed.
Errors
New BggTimeoutError and BggRateLimitError (both extend BggApiError) are thrown when
the networkError / rateLimited retry buckets exhaust. Existing
instanceof BggApiError checks keep matching — see Errors.
Usage
In Node.js (commonjs) environment
const { BggClient } = require("bgg-ts-client");In ES environment
import { BggClient } from 'bgg-ts-client';Initialize BggClient and get singleton instance
const client = BggClient.Create({ apiKey: 'YOUR_API_KEY' });Cookie auth (private collections)
Some endpoints — most notably collection with showprivate=1 — only return data for the authenticated user. Pass a session cookie alongside your API key:
const client = BggClient.Create({
apiKey: 'YOUR_API_KEY',
cookie: 'bggusername=foo; bggpassword=...; SessionID=...',
});To obtain the cookie, log in to boardgamegeek.com in a browser, open DevTools → Application/Storage → Cookies → boardgamegeek.com, and copy the cookie header value verbatim. Cookies expire — re-issue when needed.
Note: cookie auth is realistically Node-only. Browsers will block cross-origin cookie requests to
boardgamegeek.comunless you proxy through your own server.
Retry policy
The client retries transient failures automatically. Four independent policies handle the kinds of failures BGG returns:
| Bucket | Triggers on | Default baseDelayMs | Default maxDelayMs | Default maxAttempts |
|---|---|---|---|---|
| queued | HTTP 202 (BGG queueing collection requests) | 2000 | 30000 | 8 |
| rateLimited | HTTP 429 or 503 | 2000 | 30000 | 5 |
| serverError | other HTTP 5xx | 1000 | 10000 | 3 |
| networkError | fetch/connect/timeout | 1000 | 10000 | 3 |
Each retry waits min(baseDelayMs * 2^(attempt-1), maxDelayMs) plus jitter. Override any subset; missing keys keep the defaults:
const client = BggClient.Create({
apiKey: 'YOUR_API_KEY',
retry: {
rateLimited: { maxAttempts: 8, maxDelayMs: 60000 },
},
});When retries exhaust, the client throws a typed error (see below).
Errors
All errors are subclasses of BggClientError (which carries the endpoint name and the underlying cause). Inspect the cause to disambiguate.
BggClientError— wraps everything thrown from aquery().endpoint+cause.BggApiError— HTTP error from BGG (or its error envelope at HTTP 200).statusCode+url+message.BggTimeoutError— extendsBggApiError. Thrown afternetworkErrorretries exhaust on connect/timeout failures.BggRateLimitError— extendsBggApiError. Thrown afterrateLimitedretries exhaust on HTTP 429.BggParseError— XML or DTO parse failure.rawDatatruncated to 500 chars.
instanceof BggApiError matches BggTimeoutError and BggRateLimitError too, so existing checks keep working.
Field types
DTO fields use proper TypeScript types where BGG's wire format is unambiguous: dates are Date objects (e.g. play.date, collection.status.lastmodified, forum.lastpostdate), boolean-ish XML attributes ("0"/"1", "yes"/"no") are boolean, and numeric attributes are number. Missing or malformed values are undefined rather than empty strings.
Every DTO also carries an extras: Record<string, unknown> field. When BGG adds new XML attributes or elements that the typed schema doesn't yet model, they appear here verbatim — your code can read them without waiting for a client release.
API
Interact with boardgamegeek entities using the corresponding client object and fire a request with query or queryWithProgress method.
Thing
Get boardgame, boardgame expansion, boardgame accessory, videogame, rpgitem, rpgissue information. Thing client exposes query and queryWithProgress.
Examples
const things: BggThingDto[] = await client.thing.query({ id: [174430, 35421],
videos: 1,
comments: 1,
marketplace: 1,
stats: 1,
type: "boardgame" });
// with progress handler as parameter
await client.thing.queryWithProgress({
id: [250621, 257668, 226255, 340790, 279307, 279306, 345121, 271447, 187104, 253618, 271512, 432, 68448, 173346, 346703, 302260, 239472, 172818, 231398, 202408, 267814, 267813, 191189, 267127, 281946, 264647, 2272, 230085, 31260, 247367, 256442, 161970, 6249, 181293],
videos: 1,
comments: 1,
marketplace: 1,
stats: 1,
type: "boardgame"
}, { limit: 10 }, _data => {
});
// with progress handler registered on the client itself
client.thing.progressHandler = (_data) => { };
await client.thing.queryWithProgress({
id: [250621, 257668, 226255, 340790, 279307, 279306, 345121, 271447, 187104, 253618, 271512, 432, 68448, 173346, 346703, 302260, 239472, 172818, 231398, 202408, 267814, 267813, 191189, 267127, 281946, 264647, 2272, 230085, 31260, 247367, 256442, 161970, 6249, 181293],
videos: 1,
comments: 1,
marketplace: 1,
stats: 1,
type: "boardgame"
}, { limit: 10 });Family
Get rpg, rpgperiodical, boardgamefamily information. Family client exposes query and queryWithProgress.
Examples
const families = await client.family.query({ id: [174430, 35421] });Forum List
Get a list of forums (in boardgame or family page (of the id), forums tab, left sidebars with all forums). ForumList client exposes query and queryWithProgress.
Examples
const forumlists: BggForumlistDto[] = await client.forumlist.query({ id: [8374,22184,59218,1029,2076], type: ['family']});Forum
Get a single forum.
Examples
const forum = await client.forum.query({ id: 19, page: 3 });Thread
Get a single thread.
Examples
const threads: BggThreadDto[] = await client.thread.query({ id: 2571698, minarticledate: '2021-01-03', count: 15 });User
Get public profile information about a user by username.
Examples
const users: BggUserDto[] = await client.user.query({ name: 'mattiabanned', hot: 1, top: 1 });Guild
Get a single guild.
Examples
const guilds: BggGuildDto[] = await client.guild.query({ id: 1000, members: 1, sort: 'date', page: 1 });Play
Request plays logged by a particular user or for a particular item.
Examples
const response: BggPlaysDto = await client.play.query({ username: 'mattiabanned' });
// wrapper carries BGG metadata: username, userid, total, page, termsofuse
const plays: BggPlayDto[] = response.plays;Collection
Request the collection of a particular user.
Examples
const response: BggCollectionDto = await client.collection.query({ username: 'mattiabanned', excludesubtype: ["boardgameaccessory"] });
// wrapper carries BGG metadata: totalitems, pubdate, termsofuse
const items: BggCollectionItemDto[] = response.items;
// each item's name is a plain string; sortindex is exposed separately as `nameSortindex`Search
Search BGG for items by name.
Examples
const response: BggSearchDto = await client.search.query({ query: 'gloomhaven', type: 'boardgame', exact: 1 });
// wrapper carries BGG metadata: total, termsofuse
const results: BggSearchItemDto[] = response.items;Hot
Get the current BGG hotness list.
Examples
const hot: BggHotDto[] = await client.hot.query({ type: 'boardgame' });Error Handling
All errors thrown from a query() call are wrapped in a BggClientError. Inspect cause to distinguish HTTP failures from parse failures, and use instanceof against the specific subclasses for finer control. See the Errors section above for what each class represents.
import {
BggClient,
BggClientError,
BggApiError,
BggTimeoutError,
BggRateLimitError,
BggParseError,
} from 'bgg-ts-client';
const client = BggClient.Create({ apiKey: 'YOUR_API_KEY' });
try {
const things = await client.thing.query({ id: [174430], type: 'boardgame' });
} catch (error) {
if (error instanceof BggClientError) {
console.log(error.endpoint); // e.g. "thing"
if (error.cause instanceof BggRateLimitError) {
console.log('Rate-limit retries exhausted; back off further.');
} else if (error.cause instanceof BggTimeoutError) {
console.log('Network/timeout retries exhausted.');
} else if (error.cause instanceof BggApiError) {
console.log(error.cause.statusCode, error.cause.url);
} else if (error.cause instanceof BggParseError) {
console.log(error.cause.rawData); // truncated to 500 chars
}
}
}