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 likeid,category,equipment,body_part, andtargetimages/– Static thumbnails referenced by theimagefield in each recordvideos/– Animated GIFs referenced by thegif_urlfielddata/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:
limitandoffsetmust be positive integers, withlimitcapped 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.readFileSyncondata/exercises.jsonto cache 1,324 records in memory for fast access. - Serve static media by mounting
express.staticmiddleware on/imagesand/videosroutes 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.slicewith validatedlimit(max 200) andoffsetparameters to control response payload sizes. - Validate inputs with Joi schemas to ensure
limitandoffsetare integers and to prevent malformed queries from reaching business logic. - Handle errors by returning
404status for missing IDs and400status 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:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →