![GitHub release (latest by date)](https://img.shields.io/github/v/release/krzysztof-fijolek/bgg-ts-client?color=4ea8ff&style=for-the-badge)

# bgg-ts-client

A TypeScript client for the official BoardGameGeek XML API v2.

> **Note:** This project is a fork of [boardgamegeekclient](https://github.com/LearningProcesss/boardgamegeekjsclient) by [learningprocesss](https://github.com/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

![](/docs/thing.gif)

## 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

```bash
npm i bgg-ts-client
```

```bash
yarn add bgg-ts-client
```

## Breaking 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`](./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
  `BggClient` and the error classes, so consumers deep-imported types from
  `bgg-ts-client/dist/esm/dto/concrete/subdto/…`. Those internal folders
  (`dto/concrete/subdto/` and `dto/concrete/paginated/`) **have been removed** — deep
  imports into them break. Import from the root instead:

  ```ts
  // 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 the
  `BggPlaysPlay*` family. Their data now lives inline on the endpoint DTOs listed below.

- **`IDtoParser`** no longer exposes `jsonToDto` or a `parser` field — use the synchronous
  `parse(parsedXml)`.

- **Dependencies:** `jackson-js` removed; `fast-xml-parser` bumped `^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** — use `names: BggNameDto[]` (each with
  `type: 'primary' | 'alternate'`, `sortindex`, `value`). `alternateNames: string[]` is
  retained and derived from `names`.
- `statistics` is **flattened**: was `BggStatisticsPaginatedDto` with a nested
  `.ratings` object (`statistics.ratings.average`, `statistics.ratings.averageweight`,
  `statistics.ratings.ranks`). Now `statistics: BggThingStatsDto | undefined` with those
  fields **directly on it** — `statistics.average`, `statistics.averageweight`,
  `statistics.ranks`. The `.ratings` layer is gone.
- `marketplacelistings: BggThingMarketlistingsDto[]` **renamed** to
  `marketplace: BggMarketplaceItemDto[] | undefined`.
- `videos` was a paginated wrapper (`BggThingVideoPaginatedDto`) → now
  `videos: BggVideoDto[] | undefined`.
- `comments: BggThingCommentPaginatedDto` → `comments: BggThingCommentsDto | undefined`
  (same `{ totalitems, page, items }` shape).
- `polls` restructured: `BggPollDto[]` is now a **discriminated union on `name`**
  (`suggested_numplayers` | `suggested_playerage` | `language_dependence`), `totalvotes`
  is a `number`, and `results` is typed per poll. **`resultItemList` is removed** — read
  `results` directly.
- `pollSummary: any` → `pollSummary: BggPollSummaryDto[] | undefined`.
- Previously-required scalars (`type`, `description`, `yearpublished`, …) are now
  `| undefined`; guard them under `strictNullChecks`.

### 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** — `BggCollectionStatusDto` flags (`own`, `prevowned`, `fortrade`, `want`,
  `wanttoplay`, `wanttobuy`, `wishlist`, `preordered`) `"0"`/`"1"` → `boolean | undefined`;
  `lastmodified` `string` → `Date | undefined`. New `nameSortindex` field.
- **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 as
  `new Date(0)`, not normalized away.
- `BggLinkDto.inbound: boolean | undefined` is 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](#errors).

## Usage

In Node.js (commonjs) environment

```js
const { BggClient } = require("bgg-ts-client");
```

In ES environment

```js
import { BggClient } from 'bgg-ts-client';
```

Initialize BggClient and get singleton instance

```js
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:

```ts
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.com` unless 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:

```ts
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 a `query()`. `endpoint` + `cause`.
- **`BggApiError`** — HTTP error from BGG (or its error envelope at HTTP 200). `statusCode` + `url` + `message`.
- **`BggTimeoutError`** — extends `BggApiError`. Thrown after `networkError` retries exhaust on connect/timeout failures.
- **`BggRateLimitError`** — extends `BggApiError`. Thrown after `rateLimited` retries exhaust on HTTP 429.
- **`BggParseError`** — XML or DTO parse failure. `rawData` truncated 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

```ts
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

```js
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

```ts
const forumlists: BggForumlistDto[] = await client.forumlist.query({ id: [8374,22184,59218,1029,2076], type: ['family']});
```

## Forum

Get a **single** forum.

### Examples

```ts
const forum = await client.forum.query({ id: 19, page: 3 });
```

## Thread

Get a **single** thread.

### Examples

```ts
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

```ts
const users: BggUserDto[] = await client.user.query({ name: 'mattiabanned', hot: 1, top: 1 });
```

## Guild

Get a **single** guild.

### Examples

```ts
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

```ts
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

```ts
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

```ts
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

```ts
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](#errors) section above for what each class represents.

```ts
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
    }
  }
}
```
