Library / SDK
jhlywa/chess.js avatar
jhlywa/chess.js

chess.js: Move Generation and Validation in TypeScript

A TypeScript chess library for chess move generation/validation, piece placement/movement, and check/checkmate/draw detection

4,413 stars989 forksTypeScriptBSD-2-Clause

At a glance

What is it?
chess.js is a BSD-2-Clause TypeScript library that handles legal move generation, FEN and PGN handling, and check, checkmate and draw detection. It deliberately stops short of engine play, which is the boundary worth understanding before you adopt it.
Who is it for?
Adopt chess.js when you need correct move legality, FEN or PGN handling and game-over detection inside a JavaScript or TypeScript application, and you are supplying the opponent logic yourself. Skip it if you expect the package to choose moves for you, and skip it if you are targeting a Node runtime older than 20.
Can I use it commercially?
Yes. BSD-2-Clause is a permissive licence: you can use, modify and sell software built on it, as long as you keep its copyright and licence notices.
Is it still maintained?
Yes. The repository last received commits 50 days ago.
What is it written in?
Mainly TypeScript, according to GitHub's language statistics.

Answers come from the project's GitHub data, last synced on September 30, 2026, and from our analysis. They are not legal advice.

Editorial analysis

What chess.js Is Responsible For, and What It Refuses To Do

The package description in package.json is unusually honest about scope: "TypeScript library for chess move generation, validation, execution, and game state - everything but the AI." That sentence is the whole adoption decision. chess.js knows the rules of chess. It does not know which move is good.

The README frames the same boundary: chess.js is "used for chess move generation/validation, piece placement/movement, and check/checkmate/stalemate detection - basically everything but the AI." So the library answers questions like which moves are legal from a given position, whether the position after a move is checkmate or stalemate, and whether a draw condition applies. It will not evaluate a position, search a game tree or suggest a move.

The audience follows from that. If you are building a board interface, a PGN viewer, a puzzle trainer, a server that validates moves submitted by clients, or a test harness for an engine you wrote separately, chess.js covers the rules layer. If you are building an engine, chess.js is the wrong component, and the README says so without hedging.

The Chess Class, FEN and PGN as the Data Flow

The public surface shown in the README is a single Chess class. You construct an instance, ask it for moves, apply one, and inspect the resulting state. The example in the README plays a random game in a loop: it calls chess.moves(), picks one at random, calls chess.move(move), and repeats until chess.isGameOver() returns true. At the end it prints chess.pgn().

That loop exposes the architecture. The object holds the current position internally, and moves() computes the legal move list for that position rather than for the whole game. Each move() call mutates the position and returns information about the move that was made. isGameOver() is a state query over the current position, covering checkmate, stalemate and draw conditions. pgn() serialises the move history accumulated since construction.

The package keywords are chess, fen, pgn and typescript, which matches what the library actually exchanges with the outside world: FEN strings for positions and PGN for game records. Those two formats are the integration points. If your application already speaks FEN or PGN, you are feeding chess.js in its native vocabulary.

The repository layout backs this up. There is a src/ directory, a __tests__/ directory, a benchmarks/ directory, and a peggy.config.mjs at the top level. Peggy is a parser generator, and the build script runs "peggy -c peggy.config.mjs" before TypeScript compilation. The clean script removes src/pgn.js and src/pgn.d.ts, which are generated artifacts. PGN parsing is therefore generated from a grammar rather than hand-written, which is a reasonable design choice for a format with as many edge cases as PGN, though it also means the PGN grammar is a separate thing to review when something fails to parse.

Installing chess.js and Playing a Random Game

Installation is a single npm command. The README gives it as npm install chess.js, and package.json declares the package name as chess.js with version 1.4.0. The engines field requires Node 20.0.0 or newer, so check your runtime before installing; older Node versions are outside what the package declares support for.

bash
npm install chess.js

The README's example is a complete program. It imports the Chess class, constructs an instance with the default starting position, and loops until the game ends.

ts
import { Chess } from 'chess.js'

const chess = new Chess()

while (!chess.isGameOver()) {
  const moves = chess.moves()
  const move = moves[Math.floor(Math.random() * moves.length)]
  chess.move(move)
}
console.log(chess.pgn())

What you should see is a PGN string printed to the console describing a complete, legal game that ended in checkmate, stalemate or a draw. Because the move selection is random, the game differs on each run. This example is also the fastest way to confirm the install worked: if the import resolves and the loop terminates, move generation, move execution, game-over detection and PGN serialisation are all functioning.

Where chess.js Stops Being the Right Tool

The absence of an AI is not a gap in chess.js, it is the product boundary, but it has practical consequences that surprise people. A random-move loop is the only opponent the library itself can produce. Anything stronger requires a separate engine, and chess.js offers no evaluation function, no search, no opening book and no interface for plugging one in. If your requirement is "the computer plays well," chess.js is the wrong starting point.

The Node version floor is a second constraint. The engines field says node >=20.0.0. Projects pinned to older runtimes will need to upgrade Node or pick a different library, and the README does not document a fallback build for older engines.

PGN parsing is the third area to watch. Because the parser is generated from a Peggy grammar, the failure modes when you feed it a malformed or unusual PGN come from that grammar. The README does not document how parse errors surface, and it does not describe rollback behaviour if a move fails partway through. The clean script's removal of generated src/pgn.js and src/pgn.d.ts files also means anyone building from source must run the parser step; a build that skips it will not have those modules.

Finally, the README is a quick example, not a reference. It explicitly points to https://jhlywa.github.io/chess.js for full documentation. Anything you need beyond construction, moves(), move(), isGameOver() and pgn() has to come from that site, and this article cannot speak to what it contains.

chess.js Against a Full Engine Library

The natural comparison is with a chess engine library rather than another rules library. Stockfish, distributed as a separate engine binary and driven over the UCI protocol, is the reference point most people reach for when they want a computer opponent. The difference in approach is total: Stockfish searches a game tree and evaluates positions to pick a move, while chess.js only enumerates the moves that are legal.

That makes them complements rather than substitutes. A common arrangement is chess.js for the board state, legality and PGN, with a separate engine process consulted for move selection. chess.js does not ship UCI bindings, so that wiring is yours to build.

If you compare chess.js to another JavaScript rules library, the distinctions to check are the API shape, whether PGN parsing is generated or hand-written, and the runtime floor. chess.js sets that floor at Node 20 and publishes ESM and CJS builds plus TypeScript type declarations, with main pointing at dist/cjs/chess.js, module at dist/esm/chess.js and types at dist/types/chess.d.ts. Bundler-friendly output and shipped types are part of what you are choosing.

Maintenance, Licence and the Cost of Upgrading

The repository is not archived, and the last push was on 2026-08-11, which is recent enough that the project is not dormant. Releases have been steady: v1.3.0 on 2025-05-30, v1.3.1 on 2025-05-31, and v1.4.0 on 2025-06-14. The version in package.json matches the v1.4.0 tag.

Upgrade cost is low for most consumers because the library is self-contained. There are no runtime dependencies listed in package.json, only devDependencies for the build and test toolchain (TypeScript, Rollup, Vitest, ESLint, Prettier, Peggy, api-extractor). That means a version bump does not drag in a dependency tree you have to audit, and it means the published artifact is small in terms of transitive risk.

The maintenance work falls on the project, not on you, but there is a build pipeline to be aware of if you vendor or fork it. The check script runs formatting, linting, vitest, a full build and api-extractor, and api-extractor is configured through api-extractor.json. The project tracks its public API surface, which is a signal that breaking changes are treated deliberately rather than accidentally. For a fork, that also means an API change you make locally can fail the api:check step until you run api:update.

The licence is BSD-2-Clause, declared in both package.json and the LICENSE file at the repository root. It is a permissive licence that permits use in closed-source products. This is a description of the licence identifier, not legal advice; read the LICENSE text and consult counsel for your own situation.

Editorial conclusion

Adopt chess.js when you need correct move legality, FEN or PGN handling and game-over detection inside a JavaScript or TypeScript application, and you are supplying the opponent logic yourself. Skip it if you expect the package to choose moves for you, and skip it if you are targeting a Node runtime older than 20. Before committing, read the hosted documentation at jhlywa.github.io/chess.js for the exact constructor options and the methods on the Square, Move and Piece types, because the README does not enumerate them. Then check the changelog for v1.4.0 to see whether any API you plan to call changed.

Frequently asked questions

Is there a CDN for chess.js?

The README documents installation from NPM with npm install chess.js and does not mention a CDN. The package does publish an ESM build at dist/esm/chess.js and a CJS build at dist/cjs/chess.js, which are the module entry points a browser bundler would resolve.

How do I install chess.js?

Run npm install chess.js. The package requires Node 20.0.0 or newer according to the engines field in package.json.

How do I use chess.js?

Import the Chess class, construct an instance, and call methods such as moves(), move(), isGameOver() and pgn(). The README's example plays a random game by looping over chess.moves() until chess.isGameOver() returns true.

What is chess.js?

It is a TypeScript chess library for move generation and validation, piece placement and movement, and check, checkmate, stalemate and draw detection. The README describes it as everything but the AI.

Official sources

  1. jhlywa/chess.js on GitHub
  2. License: BSD-2-Clause
  3. Project website
  4. README
  5. Releases
Add this badge to your README

If you maintain this project, the badge below links readers to this analysis and shows its maintenance status from the daily GitHub snapshot. Paste the markdown into your README; add ?metric=license or ?metric=stars to the image URL for a different field.

Add this badge to your README

markdown
[![Hysen Labs](https://hysenlabs.com/badge/jhlywa-chess-js.svg)](https://hysenlabs.com/projects/jhlywa-chess-js)