# How to Build a REST API with Express.js Using the Exercises Dataset: A Complete Guide

> Learn to build a production-ready REST API with Express.js in minutes using the exercises dataset. This guide covers filtering, pagination, and media delivery.

- Repository: [Hasan Emir Yıldırım/exercises-dataset](https://github.com/hasaneyldrm/exercises-dataset)
- Tags: how-to-guide
- Published: 2026-07-31

---

**You can build a production-ready REST API in minutes by loading the [`data/exercises.json`](https://github.com/hasaneyldrm/exercises-dataset/blob/main/data/exercises.json) file from the hasaneyldrm/exercises-dataset repository into an Express.js server that handles filtering, pagination, and static media delivery.**

The **hasaneyldrm/exercises-dataset** repository contains a comprehensive fitness dataset with 1,324 exercise records, each including multilingual instructions, metadata, and associated media files. Building an Express.js REST API to expose this data requires minimal configuration while providing full CRUD-style access to the JSON structure and direct serving of thumbnails and GIF animations.

## Dataset Structure and Architecture

The repository organizes data and media assets separately. The core dataset resides in [`data/exercises.json`](https://github.com/hasaneyldrm/exercises-dataset/blob/main/data/exercises.json), while visual assets live in dedicated directories.

**Key files to integrate:**

- [`data/exercises.json`](https://github.com/hasaneyldrm/exercises-dataset/blob/main/data/exercises.json) – Contains 1,324 exercise objects with fields like `id`, `category`, `equipment`, `body_part`, and `target`
- `images/` – Static thumbnails referenced by the `image` field in each record
- `videos/` – Animated GIFs referenced by the `gif_url` field
- [`data/exercises.schema.json`](https://github.com/hasaneyldrm/exercises-dataset/blob/main/data/exercises.schema.json) – JSON Schema for validation reference

**Recommended project structure:**

```text
my-exercises-api/
├── src/
│   ├── server.js          # Express application entry point

│   └── routes/
│        └── exercises.js    # Route handlers and business logic

├── data/
│   └── exercises.json       # Dataset (copied from repository)

├── public/
│   ├── images/             # Thumbnail assets

│   └── videos/             # GIF animations

└── package.json

```

## Project Setup and Dependencies

Initialize a Node.js project and install the required packages. The implementation uses ES modules for modern JavaScript syntax.

**[`package.json`](https://github.com/hasaneyldrm/exercises-dataset/blob/main/package.json) configuration:**

```json
{
  "name": "exercises-api",
  "version": "1.0.0",
  "description": "REST API for the Exercises Dataset",
  "main": "src/server.js",
  "type": "module",
  "scripts": {
    "start": "node src/server.js"
  },
  "dependencies": {
    "cors": "^2.8.5",
    "express": "^4.19.2",
    "joi": "^17.13.0"
  }
}

```

Install dependencies and copy the dataset:

```bash
npm install
cp -r /path/to/exercises-dataset/data ./data
cp -r /path/to/exercises-dataset/images ./public/images
cp -r /path/to/exercises-dataset/videos ./public/videos

```

## Configuring the Express Server

Create [`src/server.js`](https://github.com/hasaneyldrm/exercises-dataset/blob/main/src/server.js) to initialize the Express application, enable CORS for browser-based clients, and mount static file serving for media assets.

**[`src/server.js`](https://github.com/hasaneyldrm/exercises-dataset/blob/main/src/server.js) implementation:**

```javascript
import express from 'express';
import cors from 'cors';
import path from 'path';
import { fileURLToPath } from 'url';
import exercisesRouter from './routes/exercises.js';

const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);

const app = express();
const PORT = process.env.PORT || 3000;

// Enable CORS for cross-origin browser requests
app.use(cors());

// Serve static media files directly from repository assets
app.use('/images', express.static(path.join(__dirname, '..', 'public', 'images')));
app.use('/videos', express.static(path.join(__dirname, '..', 'public', 'videos')));

// Mount API routes
app.use('/exercises', exercisesRouter);

// Health check endpoint
app.get('/', (req, res) => {
  res.send('Exercises API is running');
});

app.listen(PORT, () => {
  console.log(`Server listening on http://localhost:${PORT}`);
});

```

The `express.static` middleware maps the `/images` and `/videos` routes directly to the repository's media folders, allowing clients to access files using the URLs stored in the JSON records.

## Implementing RESTful Endpoints

Create [`src/routes/exercises.js`](https://github.com/hasaneyldrm/exercises-dataset/blob/main/src/routes/exercises.js) to handle data loading, validation, and route logic. The module reads [`data/exercises.json`](https://github.com/hasaneyldrm/exercises-dataset/blob/main/data/exercises.json) into memory at startup using `fs.readFileSync`, then applies filtering and pagination to the in-memory array.

**[`src/routes/exercises.js`](https://github.com/hasaneyldrm/exercises-dataset/blob/main/src/routes/exercises.js) implementation:**

```javascript
import { Router } from 'express';
import Joi from 'joi';
import path from 'path';
import { fileURLToPath } from 'url';
import fs from 'fs';

const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
const dataPath = path.resolve(__dirname, '..', '..', 'data', 'exercises.json');

// Load dataset once at startup
const exercises = JSON.parse(fs.readFileSync(dataPath, 'utf-8'));

const router = Router();

// Validation schema for query parameters
const listSchema = Joi.object({
  limit: Joi.number().integer().min(1).max(200).default(50),
  offset: Joi.number().integer().min(0).default(0),
  category: Joi.string(),
  equipment: Joi.string(),
  body_part: Joi.string(),
  target: Joi.string(),
}).unknown(true);

// GET /exercises - List with filters and pagination
router.get('/', (req, res) => {
  const { error, value } = listSchema.validate(req.query);
  if (error) return res.status(400).json({ error: error.message });

  const { limit, offset, ...filters } = value;

  // Apply dynamic filtering
  let result = Object.entries(filters).reduce((acc, [key, val]) => {
    return acc.filter((ex) => ex[key] && ex[key] === val);
  }, exercises);

  const total = result.length;
  result = result.slice(offset, offset + limit);

  res.json({
    total,
    limit,
    offset,
    data: result,
  });
});

// GET /exercises/:id - Single exercise by ID
router.get('/:id', (req, res) => {
  const ex = exercises.find((e) => e.id === req.params.id);
  if (!ex) return res.status(404).json({ error: 'Exercise not found' });
  res.json(ex);
});

export default router;

```

### Filtering and Pagination Logic

The **filtering mechanism** uses `Array.reduce` to chain `Array.filter` calls based on provided query parameters. Any field present in the dataset (such as `category`, `equipment`, `body_part`, or `target`) can function as a filter criterion.

**Pagination** implements offset-based slicing using `Array.slice(offset, offset + limit)`, returning a subset of results along with metadata describing the total available records.

### Validation Strategy

The **Joi schema** enforces type safety on incoming requests:
- `limit` and `offset` must be positive integers, with `limit` capped at 200 to prevent excessive payload sizes
- String filters accept any value, allowing flexible filtering without hardcoding valid options
- Unknown properties are permitted (`unknown(true)`) to support future dataset schema extensions

## Running and Testing the API

Start the server and verify functionality using curl or a REST client.

```bash
npm start

```

**Test the endpoints:**

- **List all exercises:** `GET http://localhost:3000/exercises`
- **Paginated results:** `GET http://localhost:3000/exercises?limit=10&offset=20`
- **Filtered by category:** `GET http://localhost:3000/exercises?category=chest`
- **Multiple filters:** `GET http://localhost:3000/exercises?category=chest&equipment=barbell`
- **Single record:** `GET http://localhost:3000/exercises/0001`
- **Static media:** `GET http://localhost:3000/images/0001-2gPfomN.jpg`

The [`setup.html`](https://github.com/hasaneyldrm/exercises-dataset/blob/main/setup.html) file included in the original repository provides additional context for backend generation, including SQL insert statements and LLM prompts that complement this Express implementation.

## Summary

- **Load the dataset** once at startup using `fs.readFileSync` on [`data/exercises.json`](https://github.com/hasaneyldrm/exercises-dataset/blob/main/data/exercises.json) to cache 1,324 records in memory for fast access.
- **Serve static media** by mounting `express.static` middleware on `/images` and `/videos` routes pointing to the repository's asset folders.
- **Implement filtering** by reducing the exercises array against query parameters, supporting dynamic filtering by any JSON field.
- **Add pagination** using `Array.slice` with validated `limit` (max 200) and `offset` parameters to control response payload sizes.
- **Validate inputs** with Joi schemas to ensure `limit` and `offset` are integers and to prevent malformed queries from reaching business logic.
- **Handle errors** by returning `404` status for missing IDs and `400` status for validation failures.

## Frequently Asked Questions

### How do I filter exercises by specific equipment?

Append the `equipment` query parameter to the `/exercises` endpoint. For example, `GET /exercises?equipment=dumbbell` returns only exercises requiring dumbbells. The API supports any field present in the JSON records, including `category`, `body_part`, and `target`, and you can combine multiple filters using `&` separators.

### Can I modify the API to use a database instead of the JSON file?

Yes. Replace the `fs.readFileSync` call with a database connection using packages like `pg` for PostgreSQL or `mongoose` for MongoDB. Keep the route handlers in [`src/routes/exercises.js`](https://github.com/hasaneyldrm/exercises-dataset/blob/main/src/routes/exercises.js) structurally identical—simply swap the `Array.filter` and `Array.slice` operations for SQL `WHERE` clauses and `LIMIT/OFFSET` statements or MongoDB `.find()` and `.skip()/.limit()` chains.

### How do I deploy this API to production?

Set the `PORT` environment variable to match your hosting platform's requirements (e.g., `process.env.PORT`), remove the wildcard CORS configuration in favor of specific origin whitelisting, and add middleware like `helmet` for security headers. Serve static media through a CDN or cloud storage rather than the Express server if expecting high traffic volumes.

### What validation is performed on the exercise IDs?

The single resource endpoint (`GET /exercises/:id`) performs an exact string match against the `id` field in each exercise record using `Array.find`. If no match exists, it returns a `404` status with an error message. The ID parameter itself is not validated against a specific pattern, allowing the API to work with any ID format present in the dataset.