Chess Web Programming: Part Seven: Lichess API

Working with the Lichess API

My journey into programming chess applications started with Chessboard Magic, a personal project that turned into a platform with over 34 unique chess-based applications. This blog continues my Chess Web Programming series, diving into one of the most exciting aspects of modern development: working with APIs. In this installment, we’ll explore the Lichess API, learn how to interact with it, and create a React application to fetch and display Games from any Lichess User in a structured and beginner-friendly way.

Recap of the Previous Parts

Before we dive into the Lichess API, here’s a quick recap of the earlier blogs in the series:

  1. Getting Started: Using react-chessboard and chess.js to set up a functional chessboard.
  2. Integrating Stockfish: Enhancing the application by suggesting the best move using Stockfish.
  3. Deploying Your Application: A guide to hosting your chess application online.
  4. Customizing the Chessboard: Styling and implementing features like custom piece sets and square highlighting.
  5. Game Review: Adding features like move highlighting and navigation for game analysis.
  6. Essential Resources: Tools, libraries, and learning materials to support chess application development.

Note: This blog assumes that you’ve followed the previous parts of the series, particularly Blog 1, which covers setting up the foundational tools and concepts. You should already have the necessary software installed (e.g., Node.js, Visual Studio Code) and a basic understanding of React web programming. If you haven’t reviewed Blog 1 yet, we recommend starting there to ensure you’re set up for success.

What Are APIs?

APIs (Application Programming Interfaces) act as intermediaries that allow software applications to communicate with one another. They provide a set of rules and protocols for accessing a service or application’s features or data. For example, when you check the weather on your phone, the app fetches the data from a server using APIs. Similarly, we’ll use the Lichess API to retrieve chess game data.

What Is a RESTful API?

A RESTful API (Representational State Transfer API) is a type of API that adheres to specific architectural principles. It uses standard HTTP methods such as GETPOSTPUT, and DELETE to manage resources (like game data or user profiles).
RESTful APIs:

  • Are stateless, meaning every request from the client to the server must contain all the information needed to process the request.
  • Use endpoints (specific URLs) to access different resources.

For instance, the Lichess API uses endpoints to provide access to specific types of data. Here’s an example:

  • Endpointhttps://lichess.org/api/games/user/{username}
  • Purpose: Fetches games played by a given Lichess user.

In this blog, we’ll focus on the Games endpoint to fetch and display a user’s game history.

What Is HTTP?
HTTP (HyperText Transfer Protocol) is the foundation of communication on the web. It defines how messages are formatted and transmitted between clients (like a browser or application) and servers. For example, when your application sends a request to the Lichess API, it uses HTTP to communicate. Common HTTP methods include:

  • GET: Retrieve data from a server.
  • POST: Send data to a server.
  • PUT: Update data on a server.
  • DELETE: Remove data from a server.

The Lichess API

The Lichess API is a RESTful API that gives developers access to a wealth of chess-related data. Here are some of its features:

  • User Data: Retrieve information about a specific user, such as their rating and games played.
  • Game Data: Access complete game histories, including moves, openings, and final positions.
  • Tournaments and Puzzles: Fetch details about tournaments or solve chess puzzles programmatically.

In this tutorial, we’ll use the Games endpoint to:

  1. Fetch a specified number of games for a given user.
  2. Include details like the opening name, moves, and final position (FEN).

By understanding how endpoints work, you’ll be able to explore other parts of the Lichess API and integrate additional features into your application in the future.

Setting Up a React Application

In this section, we’ll create a React application from scratch using Create React App. This will serve as the foundation for our Lichess API integration.

Step 1: Create a New React Application

React is a popular JavaScript library for building user interfaces. To get started, we’ll use a tool called Create React App, which sets up a new project with all the necessary configurations and dependencies pre-installed.

  1. Open Your Terminal
    You’ll need a terminal or command-line tool to run commands. On Windows, you can use Command Prompt or PowerShell. On macOS or Linux, use the built-in terminal.
  2. Run the Create React App Command
    In your terminal, type npx create-react-app lichess-api-app. The npx command ensures that you’re using the latest version of Create React App, while lichess-api-app is the name of your project folder. This command downloads and installs all the dependencies required for a React project. It might take a few minutes depending on your internet connection.
  3. Navigate Into Your Project Folder
    Once the installation is complete, use the cd lichess-api-app command to navigate into the folder where your new React project is located.
  4. What’s Inside the Folder?
    Your project folder contains several files and directories:
    • node_modules folder with all the dependencies installed for your project.
    • public folder that includes static assets like the index.html file.
    • src folder, which is the main location for your React components and logic.
    • package.json file, which lists project metadata and dependencies.
    • README.md file, which contains instructions and information about your project.

At this stage, your new React application is ready, and you’re set to begin building your project.

Open the Project in VS Code

Once your React application is created, the next step is to open it in a code editor. We’ll use Visual Studio Code (VS Code), a popular editor for web development.

  1. Launch VS Code
    Open Visual Studio Code from your operating system’s application menu.
  2. Add the Project Folder to Workspace
    In VS Code:
    • Click on File in the top menu bar.
    • Select Add Folder to Workspace.
    • In the file explorer that appears, navigate to the folder where you created your React project (lichess-api-app).
    • Select the folder and click Add.
    This step ensures that all files and folders in your project are accessible from within VS Code.
  3. Explore the Workspace
    Once the folder is added, you’ll see the project structure in the VS Code Explorer panel on the left. Key elements include:
    • src folder: This is where you’ll write your React code.
    • public folder: Contains the index.html file, where your React application is injected.
    • package.json: Lists project dependencies and configuration.
  4. Open the Terminal in VS Code
    To run commands directly from VS Code:
    • Go to the top menu and select View Terminal.
    • A terminal window will open at the bottom of the editor, starting in your project folder.
  5. Verify Everything is Set Up
    In the terminal, start the development server by typing npm start. This will run your React application and open it in your default web browser at http://localhost:3000.
    If you see the default React welcome page in your browser, everything is set up correctly.

Note: If you have another server running on your machine (for example, another React project), you may encounter a message like:
“Something is already running on port 3000.”
In this case, you will be given two options:

  1. Use a Different Port: React will prompt you to run the application on a different port, such as http://localhost:3001. You can press Y to confirm.
  2. Shut Down the Other Server: If you prefer to run your application on port 3000, you’ll need to stop the other server. To do this, go to the terminal where the other server is running and press Ctrl + C (or Cmd + C on macOS) to terminate it.

Introducing Material UI

In this section, we’ll enhance the user interface of our React application using Material UI, a popular library of pre-styled and customizable components. Material UI allows us to quickly build professional, modern-looking interfaces without having to write extensive CSS. By the end of this section, we’ll have a clean and responsive form with the following features:

  • An input field for the Lichess username.
  • An input field for the number of games to fetch (default set to 50).
  • A button to trigger the API call and fetch the games.

This form will lay the groundwork for integrating the Lichess API in the next section.

Why Use Material UI?

Material UI (MUI) is based on Google’s Material Design principles, focusing on clean, consistent, and user-friendly interfaces. Here are some reasons to use it:

  1. Pre-Styled Components: Includes a library of pre-designed components like buttons, tables, and input fields.
  2. Customizability: You can easily tweak the styles using inline styling, theming, or CSS overrides.
  3. Accessibility: Material UI components are built with accessibility in mind, making your application more user-friendly.
  4. Ease of Use: You can achieve professional designs with minimal effort, reducing the need to write custom CSS.

By using Material UI, we can focus more on building the application logic rather than spending time on UI design from scratch.

Step 1: Install Material UI

To begin using Material UI, we need to install the required packages. These include:

  • [@mui](/@/mui)/material: The core Material UI component library.
  • [@emotion](/@/emotion)/react and [@emotion](/@/emotion)/styled: Required for styling the Material UI components.

Install these packages by running the following command in your terminal:

npm install [@mui](/@/mui)/material [@emotion](/@/emotion)/react [@emotion](/@/emotion)/styled

The [@emotion](/@/emotion)/react and [@emotion](/@/emotion)/styled packages are necessary for Material UI’s styling to work. Once installed, you can start using Material UI components in your project.

Step 2: Create the Basic Form

Now, we’ll set up a basic form in App.js with:

  • An input field for the Lichess username.
  • An input field for the number of games to fetch (default 50).
  • A button to fetch games.

Replace the content of src/App.js with the following:

import React, { useState } from 'react';
import { TextField, Button, Container, Typography } from '[@mui](/@/mui)/material';

function App() {
  const [username, setUsername] = useState('');
  const [numberOfGames, setNumberOfGames] = useState(50);

  const fetchGames = () => {
    console.log(`Fetching ${numberOfGames} games for ${username}`);
  };

  return (
    <Container>
      <Typography variant="h4" gutterBottom>
        Lichess Game Fetcher
      </Typography>
      <TextField
        label="Lichess Username"
        variant="outlined"
        fullWidth
        margin="normal"
        value={username}
        onChange={(e) => setUsername(e.target.value)}
      />
      <TextField
        label="Number of Games"
        variant="outlined"
        type="number"
        fullWidth
        margin="normal"
        value={numberOfGames}
        onChange={(e) => setNumberOfGames(e.target.value)}
      />
      <Button
        variant="contained"
        color="primary"
        onClick={fetchGames}
        style={{ marginTop: '20px' }}
      >
        Fetch Games
      </Button>
    </Container>
  );
}

export default App;

Step 3: Understanding the Code

  • Container: A layout component from Material UI that centers the content and adds padding.
  • Typography: Used to style text elements like headings and paragraphs. We used it here to create a title for the application.
  • TextField: A Material UI input component. We added two text fields:
    1. For entering the Lichess username.
    2. For specifying the number of games to fetch.
  • Button: A Material UI button component. When clicked, it triggers the fetchGames function.

The useState hooks manage the form’s state:

  • username stores the entered Lichess username.
  • numberOfGames stores the number of games to fetch.

The fetchGames function logs the entered values to the console. We’ll enhance this function in the next section to fetch data from the Lichess API.

Step 4: Preview the Form

Save your changes and check the browser. You should see:

  • A title labeled “Lichess Game Fetcher.”
  • Two input fields for the username and the number of games.
  • A button labeled “Fetch Games.”
image.png

At this stage, you’ve built a functional form using Material UI. In the next section, we’ll connect this form to the Lichess API to fetch and display game data.

Fetching and Displaying Games

In this section, we’ll integrate the Lichess API into our React application. The goal is to fetch a list of chess games based on the input values (Lichess username and number of games) and display the data in a clean and structured format using a Material UI Table. We’ll enhance the table to include player details, results, game dates, opening names, final positions, and the sequence of moves.

Step 1: Understand the Lichess API Endpoint

The Games API endpoint retrieves games played by a specific Lichess user. The structure of the endpoint is as follows:
Endpoint:
https://lichess.org/api/games/user/{username}
Query parameters:

  • max: Specifies the maximum number of games to fetch (user input, default 50).
  • opening: Includes the opening name in the response if set to true.
  • moves: Retrieves the complete move list if set to true.
  • lastFen: Includes the final board position in FEN format if set to true.

Step 2: Fetch Games from the Lichess API

To retrieve games in NDJSON format:

  1. Build the API URL dynamically with user inputs.
  2. Set the Accept header to request NDJSON.
  3. Parse the NDJSON response and store it in a state variable for rendering.

Here’s the function to fetch games:

const fetchGames = async () => {
  if (!username) {
    alert("Please enter a Lichess username."); // Validate username input.
    return;
  }

  const url = `https://lichess.org/api/games/user/${username}?max=${numberOfGames}&opening=true&moves=true&lastFen=true`;

  try {
    const response = await fetch(url, {
      headers: { Accept: 'application/x-ndjson' }, // Request NDJSON format.
    });

    const text = await response.text(); // Read the NDJSON response as plain text.
    const parsedGames = text
      .trim() // Remove extra spaces or newlines.
      .split('\n') // Split the NDJSON by newline to separate game data.
      .map((line) => JSON.parse(line)); // Parse each line into a JSON object.
    setGames(parsedGames); // Update the state with the parsed game data.
  } catch (error) {
    console.error("Error fetching games:", error); // Log any errors to the console.
    alert("Failed to fetch games. Please try again."); // Show an error message to the user.
  }
};

Explanation of code:

  • Validates the input to ensure a username is provided.
  • Dynamically constructs the API URL using the username and number of games, with parameters to include opening names, moves, and the final board position in FEN format.
  • Sends a GET request to the Lichess API, requesting data in NDJSON format by setting the appropriate Accept header.
  • Processes the NDJSON response by:
    • Reading it as plain text.
    • Splitting it into lines (one line per game).
    • Parsing each line into a JSON object.
  • Updates the games state with the parsed data, triggering a UI update to display the results.
  • Handles errors by logging them to the console and showing an alert to the user.

Step 3: Add a Table to Display Games

We’ll use Material UI’s Table component to dynamically display the game data fetched from the Lichess API. Each row will represent a single game, with the following columns:

  • White: Displays the username and rating of the White player.
  • Black: Displays the username and rating of the Black player.
  • Result: Indicates the result of the game (e.g., “White Wins,” “Black Wins,” or “Draw”).
  • Date: Shows the date when the game was played, formatted for readability.
  • Opening: Displays the name of the opening used in the game, if available.
  • Last Position: Shows the final board position in FEN format.
  • Moves: Displays the sequence of moves played during the game.

Here’s the updated implementation of the table with inline comments explaining each part:

{games.length > 0 && ( // Check if games are available before rendering the table.
  <Table sx={{ marginTop: '20px' }}> {/* Material UI Table with spacing above */}
    <TableHead>
      <TableRow>
        <TableCell>White</TableCell> {/* Column for White player's username and rating */}
        <TableCell>Black</TableCell> {/* Column for Black player's username and rating */}
        <TableCell>Result</TableCell> {/* Column for game result */}
        <TableCell>Date</TableCell> {/* Column for game date */}
        <TableCell>Opening</TableCell> {/* Column for opening name */}
        <TableCell>Last Position</TableCell> {/* Column for final board position (FEN) */}
        <TableCell>Moves</TableCell> {/* Column for sequence of moves */}
      </TableRow>
    </TableHead>
    <TableBody>
      {/* Loop through the games array to generate a table row for each game */}
      {games.map((game, index) => (
        <TableRow key={index}> {/* Unique key for each row to optimize rendering */}
          {/* White player's details */}
          <TableCell>
            {game.players.white.user?.name || "Anonymous"} {/* Username or fallback */}
            {" "}({game.players.white.rating}) {/* Rating */}
          </TableCell>

          {/* Black player's details */}
          <TableCell>
            {game.players.black.user?.name || "Anonymous"} {/* Username or fallback */}
            {" "}({game.players.black.rating}) {/* Rating */}
          </TableCell>

          {/* Game result */}
          <TableCell>
            {game.winner
              ? game.winner === "white"
                ? "White Wins" /* If winner is White */
                : "Black Wins" /* If winner is Black */
              : "Draw" /* If no winner */}
          </TableCell>

          {/* Game date */}
          <TableCell>
            {new Date(game.createdAt).toLocaleDateString("en-US", {
              year: "numeric",
              month: "short",
              day: "numeric",
            })} {/* Format timestamp into readable date */}
          </TableCell>

          {/* Opening name */}
          <TableCell>
            {game.opening?.name || "Unknown"} {/* Display opening name or fallback */}
          </TableCell>

          {/* Final board position in FEN format */}
          <TableCell>
            {game.lastFen} {/* Display the final board position */}
          </TableCell>

          {/* Moves played in the game */}
          <TableCell>
            {game.moves ? game.moves : "Moves not available"} {/* Display moves or fallback */}
          </TableCell>
        </TableRow>
      ))}
    </TableBody>
  </Table>
)}

Explanation of the Code

  1. games.length > 0 Check:
    • Ensures that the table is only rendered if the games array contains data.
    • Prevents rendering an empty table when no games are available.
  2. Material UI Table Structure:
    • The <Table> component serves as the container for the table.
    • <TableHead> defines the table header row, with <TableCell> elements for each column title.
    • <TableBody> contains the dynamic rows generated from the games array.
  3. Looping Through the Games Array:
    • The map method is used to iterate over the games array.
    • Each iteration creates a <TableRow> representing a single game.
    • Unique keys (key={index}) are assigned to each <TableRow> for efficient rendering.
  4. Fallback Values:
    • Fallback values like "Anonymous""Unknown", or "Moves not available" ensure the application handles missing data gracefully.
  5. Dynamic Data Rendering:
    • Player details, results, dates, openings, FEN, and moves are extracted from the game object and displayed in corresponding <TableCell> elements.
  6. Date Formatting:
    • The createdAt field is a timestamp in milliseconds. It’s converted to a human-readable format using toLocaleDateString.

Here is the final code:

import React, { useState } from 'react';
import {
  TextField,
  Button,
  Container,
  Typography,
  Box,
  Table,
  TableBody,
  TableCell,
  TableHead,
  TableRow,
} from '[@mui](/@/mui)/material';

function App() {
  const [username, setUsername] = useState(''); // State for the Lichess username input.
  const [numberOfGames, setNumberOfGames] = useState(50); // State for the number of games to fetch (default is 50).
  const [games, setGames] = useState([]); // State to store the fetched games data.

  // Function to fetch games from the Lichess API.
  const fetchGames = async () => {
    // Validate input: Ensure a username has been provided.
    if (!username) {
      alert("Please enter a Lichess username.");
      return;
    }

    // Construct the API URL using the username and number of games.
    const url = `https://lichess.org/api/games/user/${username}?max=${numberOfGames}&opening=true&moves=true&lastFen=true`;

    try {
      // Make the API request, specifying that we want NDJSON data.
      const response = await fetch(url, {
        headers: { Accept: 'application/x-ndjson' },
      });

      // Read the response as text and parse the NDJSON data into an array of objects.
      const text = await response.text();
      const parsedGames = text
        .trim() // Remove any leading or trailing whitespace.
        .split('\n') // Split the NDJSON response into individual lines.
        .map((line) => JSON.parse(line)); // Parse each line as JSON.

      // Update the state with the parsed games data.
      setGames(parsedGames);
    } catch (error) {
      console.error("Error fetching games:", error); // Log errors to the console.
      alert("Failed to fetch games. Please try again."); // Show an alert to the user.
    }
  };

  return (
    <Container>
      {/* Page Title */}
      <Typography variant="h4" gutterBottom>
        Lichess API: Fetch Games
      </Typography>

      {/* Input Form */}
      <Box
        sx={{
          display: 'flex', // Display inputs and button in a row.
          alignItems: 'center', // Align items vertically.
          gap: '16px', // Add spacing between elements.
          marginTop: '20px', // Add spacing above the form.
        }}
      >
        {/* Input for Lichess username */}
        <TextField
          label="Lichess Username"
          variant="outlined"
          value={username} // Bind the value to the username state.
          onChange={(e) => setUsername(e.target.value)} // Update the username state on input change.
          style={{ flex: 2 }} // Set the size of this input field relative to the button.
        />
        {/* Input for number of games */}
        <TextField
          label="Number of Games"
          variant="outlined"
          type="number" // Restrict input to numbers.
          value={numberOfGames} // Bind the value to the numberOfGames state.
          onChange={(e) => setNumberOfGames(e.target.value)} // Update the numberOfGames state on input change.
          style={{ flex: 1 }} // Make this input smaller than the username input.
        />
        {/* Button to trigger the fetchGames function */}
        <Button
          variant="contained"
          color="primary"
          onClick={fetchGames} // Trigger the fetchGames function when clicked.
        >
          Fetch Games
        </Button>
      </Box>

      {/* Table to display fetched game data */}
      {games.length > 0 && ( // Render the table only if there are games to display.
        <Table sx={{ marginTop: '20px' }}>
          <TableHead>
            <TableRow>
              <TableCell>White</TableCell> {/* Column header for White player details */}
              <TableCell>Black</TableCell> {/* Column header for Black player details */}
              <TableCell>Result</TableCell> {/* Column header for game result */}
              <TableCell>Date</TableCell> {/* Column header for game date */}
              <TableCell>Opening</TableCell> {/* Column header for opening name */}
              <TableCell>Last Position</TableCell> {/* Column header for FEN */}
              <TableCell>Moves</TableCell> {/* Column header for game moves */}
            </TableRow>
          </TableHead>
          <TableBody>
            {/* Loop through the games array to generate a table row for each game */}
            {games.map((game, index) => (
              <TableRow key={index}> {/* Unique key for each row */}
                {/* White player details */}
                <TableCell>
                  {game.players.white.user?.name || "Anonymous"} {/* Username or fallback */}
                  {" "}({game.players.white.rating}) {/* Rating */}
                </TableCell>
                {/* Black player details */}
                <TableCell>
                  {game.players.black.user?.name || "Anonymous"} {/* Username or fallback */}
                  {" "}({game.players.black.rating}) {/* Rating */}
                </TableCell>
                {/* Game result */}
                <TableCell>
                  {game.winner
                    ? game.winner === "white"
                      ? "White Wins" /* If White is the winner */
                      : "Black Wins" /* If Black is the winner */
                    : "Draw" /* If no winner */}
                </TableCell>
                {/* Game date */}
                <TableCell>
                  {new Date(game.createdAt).toLocaleDateString("en-US", {
                    year: "numeric",
                    month: "short",
                    day: "numeric",
                  })} {/* Convert timestamp to readable date */}
                </TableCell>
                {/* Opening name */}
                <TableCell>
                  {game.opening?.name || "Unknown"} {/* Display opening name or fallback */}
                </TableCell>
                {/* Final board position (FEN) */}
                <TableCell>
                  {game.lastFen} {/* Display the FEN or fallback */}
                </TableCell>
                {/* Moves played in the game */}
                <TableCell>
                  {game.moves ? game.moves : "Moves not available"} {/* Display moves or fallback */}
                </TableCell>
              </TableRow>
            ))}
          </TableBody>
        </Table>
      )}
    </Container>
  );
}

export default App;

When you enter a Lichess Username and click on Fetch, you should not see the following:
image.png

Formatting Our Output

In this section, we’ll take the code from Section 4 and format the output into the final design. The goal is to enhance the user experience by structuring the game data into two columns:

  • Column A: Displays the final chessboard position using the react-chessboard library.
  • Column B: Contains detailed game information organized into multiple rows, such as player names and ratings, game results, date and time, opening name, and moves.

We’ll explain step by step how to achieve this transformation.

Step 1: Install react-chessboard

To display the chessboard for the final position, we’ll use the react-chessboard library. If you haven’t already installed it, run the following command in your project directory:

npm install react-chessboard

After installing, you can import the Chessboard component at the top of your file:

import { Chessboard } from "react-chessboard";

Step 2: Modify the Table Layout

In Section 4, the game data was displayed in a single-row table with columns for White, Black, Result, Date, Opening, Final Position, and Moves. We’ll now restructure the table:

  • Combine all game details into Column B as a vertical layout.
  • Use Column A to display the chessboard with the final position (lastFen).

Key Changes:

  • Replace individual <TableCell> elements for game details with a <Box> containing rows for:
    1. Player names and ratings.
    2. Game result.
    3. Date and time.
    4. Opening name.
    5. Moves.

Step 3: Configure the Chessboard in Column A

The react-chessboard library provides an interactive chessboard that can display a game position using the FEN format. For our use case:

  • Use the lastFen field from the API response to set the final position.
  • Disable dragging of chess pieces using the arePiecesDraggable={false} property.
  • Adjust the size of the chessboard to 200px using boardWidth.

Here’s how to integrate the chessboard into Column A:

<Chessboard
  position={game.lastFen || "start"} // Use the final FEN position or fallback to the starting position.
  arePiecesDraggable={false} // Disable dragging on the chessboard.
  boardWidth={200} // Set the width of the chessboard.
/>

Step 4: Organize Game Details in Column B

To format the game details:

  1. Use a Box component with flexDirection: "column" to organize rows vertically.
  2. Format each piece of information (e.g., players, result, date) using <Typography> for consistent styling.
  3. Add spacing between rows using the gap property.
<Box sx={{ display: "flex", flexDirection: "column", gap: "8px" }}>
  {/* Row 1: Players and ratings */}
  <Typography variant="h6">
    {game.players.white.user?.name || "Anonymous"} ({game.players.white.rating}) vs{" "}
    {game.players.black.user?.name || "Anonymous"} ({game.players.black.rating})
  </Typography>

  {/* Row 2: Game result */}
  <Typography variant="body1">
    Result:{" "}
    {game.winner
      ? game.winner === "white"
        ? "White Wins"
        : "Black Wins"
      : "Draw"}
  </Typography>

  {/* Row 3: Date and time played */}
  <Typography variant="body2">
    Date:{" "}
    {new Date(game.createdAt).toLocaleString("en-US", {
      year: "numeric",
      month: "short",
      day: "numeric",
      hour: "2-digit",
      minute: "2-digit",
    })}
  </Typography>

  {/* Row 4: Opening name */}
  <Typography variant="body2">
    Opening: {game.opening?.name || "Unknown"}
  </Typography>

  {/* Row 5: Moves played in the game */}
  <Typography variant="body2" sx={{ whiteSpace: "pre-wrap" }}>
    Moves: {game.moves || "Moves not available"}
  </Typography>
</Box>

Step 5: Final Code

Here’s the final code after applying all formatting:

import React, { useState } from "react";
import {
  TextField,
  Button,
  Container,
  Typography,
  Box,
  Table,
  TableBody,
  TableCell,
  TableRow,
} from "[@mui](/@/mui)/material";
import { Chessboard } from "react-chessboard"; // Import Chessboard component.

function App() {
  // State for the Lichess username input.
  const [username, setUsername] = useState("");

  // State for the number of games to fetch (default is 50).
  const [numberOfGames, setNumberOfGames] = useState(50);

  // State to store the fetched games data.
  const [games, setGames] = useState([]);

  // Function to fetch games from the Lichess API.
  const fetchGames = async () => {
    // Validate input: Ensure a username has been provided.
    if (!username) {
      alert("Please enter a Lichess username.");
      return;
    }

    // Construct the API URL using the username and number of games.
    const url = `https://lichess.org/api/games/user/${username}?max=${numberOfGames}&opening=true&moves=true&lastFen=true`;

    try {
      // Make the API request, specifying that we want NDJSON data.
      const response = await fetch(url, {
        headers: { Accept: "application/x-ndjson" },
      });

      // Read the response as text and parse the NDJSON data into an array of objects.
      const text = await response.text();
      const parsedGames = text
        .trim() // Remove any leading or trailing whitespace.
        .split("\n") // Split the NDJSON response into individual lines.
        .map((line) => JSON.parse(line)); // Parse each line as JSON.

      // Update the state with the parsed games data.
      setGames(parsedGames);
    } catch (error) {
      console.error("Error fetching games:", error); // Log errors to the console.
      alert("Failed to fetch games. Please try again."); // Show an alert to the user.
    }
  };

  return (
    <Container>
      {/* Page Title */}
      <Typography variant="h4" gutterBottom>
        Lichess API: Fetch Games
      </Typography>

      {/* Input Form */}
      <Box
        sx={{
          display: "flex", // Display inputs and button in a row.
          alignItems: "center", // Align items vertically.
          gap: "16px", // Add spacing between elements.
          marginTop: "20px", // Add spacing above the form.
        }}
      >
        {/* Input for Lichess username */}
        <TextField
          label="Lichess Username"
          variant="outlined"
          value={username} // Bind the value to the username state.
          onChange={(e) => setUsername(e.target.value)} // Update the username state on input change.
          style={{ flex: 2 }} // Set the size of this input field relative to the button.
        />
        {/* Input for number of games */}
        <TextField
          label="Number of Games"
          variant="outlined"
          type="number" // Restrict input to numbers.
          value={numberOfGames} // Bind the value to the numberOfGames state.
          onChange={(e) => setNumberOfGames(e.target.value)} // Update the numberOfGames state on input change.
          style={{ flex: 1 }} // Make this input smaller than the username input.
        />
        {/* Button to trigger the fetchGames function */}
        <Button
          variant="contained"
          color="primary"
          onClick={fetchGames} // Trigger the fetchGames function when clicked.
        >
          Fetch Games
        </Button>
      </Box>

      {/* Table to display fetched game data */}
      {games.length > 0 && (
        <Table sx={{ marginTop: "20px" }}>
          <TableBody>
            {/* Loop through the games array to generate rows for each game */}
            {games.map((game, index) => (
              <TableRow key={index}>
                {/* Column A: Chessboard */}
                <TableCell>
                  <Chessboard
                    position={game.lastFen || "start"} // Use the final FEN position or fallback to the starting position.
                    arePiecesDraggable={false} // Disable dragging on the chessboard.
                    boardWidth={200} // Set the width of the chessboard.
                  />
                </TableCell>

                {/* Column B: Game details */}
                <TableCell>
                  <Box
                    sx={{
                      display: "flex", // Use a flex container.
                      flexDirection: "column", // Stack rows vertically.
                      gap: "8px", // Add spacing between rows.
                    }}
                  >
                    {/* Row 1: Players and ratings */}
                    <Typography variant="h6">
                      {game.players.white.user?.name || "Anonymous"} ({game.players.white.rating}) vs{" "}
                      {game.players.black.user?.name || "Anonymous"} ({game.players.black.rating})
                    </Typography>

                    {/* Row 2: Game result */}
                    <Typography variant="body1">
                      Result:{" "}
                      {game.winner
                        ? game.winner === "white"
                          ? "White Wins"
                          : "Black Wins"
                        : "Draw"}
                    </Typography>

                    {/* Row 3: Date and time played */}
                    <Typography variant="body2">
                      Date:{" "}
                      {new Date(game.createdAt).toLocaleString("en-US", {
                        year: "numeric",
                        month: "short",
                        day: "numeric",
                        hour: "2-digit",
                        minute: "2-digit",
                      })}
                    </Typography>

                    {/* Row 4: Opening name */}
                    <Typography variant="body2">
                      Opening: {game.opening?.name || "Unknown"}
                    </Typography>

                    {/* Row 5: Moves played in the game */}
                    <Typography variant="body2" sx={{ whiteSpace: "pre-wrap" }}>
                      Moves: {game.moves || "Moves not available"}
                    </Typography>
                  </Box>
                </TableCell>
              </TableRow>
            ))}
          </TableBody>
        </Table>
      )}
    </Container>
  );
}

export default App;

Your final application should look like the following:
image.png

Summary

In this blog, we explored the exciting intersection of chess and web development by building a React application powered by the Lichess API. We started by understanding the basics of RESTful APIs and how they enable seamless interaction between applications. Using the Lichess API, we fetched user-specific chess games, parsed the data, and displayed it in a clean and interactive format. Along the way, we integrated Material UI for styling and react-chessboard to visually represent the final positions of games, creating an application that is both functional and visually appealing.
Our final application features a two-column layout: one column displays the final position of each game on a chessboard, while the other provides detailed information about the game, such as player names and ratings, results, openings, dates, and moves. By organizing the data in this way, we ensured a clear and user-friendly interface. We also focused on best practices, such as robust error handling and responsive design, to make the application reliable and adaptable.
This project is just the beginning of what you can create with the Lichess API. Whether you’re looking to build analysis tools, create educational apps, or experiment with interactive chessboard visualizations, the possibilities are endless. By combining your passion for chess with the skills you’ve learned here, you’re equipped to innovate and bring your ideas to life. The chessboard is your canvas—let your creativity take the lead!

Learn More

Here are the tools and resources we used in this article to build our Lichess game viewer application. Each of these is essential for recreating and understanding the project:

  • Lichess API: The Lichess API is the backbone of this project, providing access to chess game data. Explore its extensive capabilities and learn how to fetch games, puzzles, and more.
  • React: React is the framework we used to build this dynamic and interactive application. Learn how to create components, manage state, and build engaging user interfaces.
  • Material UI: Material UI is the library we used to style the application and create components like tables, text fields, and buttons. Explore its components and theming options to enhance your React apps.
  • react-chessboard: This library allowed us to display the final position of games as a static chessboard. Learn how to use it to create visually appealing chessboard components in your applications.
  • Visual Studio Code: We used VS Code as our code editor throughout this project. It’s a powerful tool for coding, debugging, and managing projects. Download it to streamline your development workflow.

Comments

Leave a Reply

Your email address will not be published. Required fields are marked *