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

You can build a production-ready REST API in minutes by loading the 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, while visual assets live in dedicated directories.

Key files to integrate:

  • 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 – JSON Schema for validation reference

Recommended project structure:

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 configuration:

{
  "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:

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 to initialize the Express application, enable CORS for browser-based clients, and mount static file serving for media assets.

src/server.js implementation:

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 to handle data loading, validation, and route logic. The module reads data/exercises.json into memory at startup using fs.readFileSync, then applies filtering and pagination to the in-memory array.

src/routes/exercises.js implementation:

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.

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 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 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 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.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →