How to Build a REST API with FastAPI Using the Exercises Dataset

You can build a REST API with FastAPI by loading the data/exercises.json file at startup, creating Pydantic models that mirror the dataset schema, and mounting the images/ and videos/ directories as static file routes.

The hasaneyldrm/exercises-dataset repository contains a self-contained JSON collection of 1,324 fitness exercises with multilingual instructions, metadata, and media references. When you build a REST API with FastAPI using this dataset, you leverage FastAPI's automatic validation against the provided data/exercises.schema.json while serving thumbnail images and GIF animations directly through ASGI static file mounts.

Project Setup and Dataset Structure

Before writing endpoints, examine the repository layout to understand how data and media are organized. The dataset resides in data/exercises.json, with a corresponding JSON Schema in data/exercises.schema.json that defines the structure for validation. Media assets are stored in images/ and videos/ directories, referenced by relative paths in each exercise record.

Key files to reference:

  • data/exercises.json – Contains all 1,324 exercise records with fields like id, name, category, equipment, and nested instructions objects
  • data/exercises.schema.json – JSON Schema for validating new or modified exercise entries
  • setup.html – Interactive developer guide in the repository root with backend integration examples
  • images/ – Directory containing 1,324 thumbnail JPG files
  • videos/ – Directory containing 1,324 animation GIF files

Creating Pydantic Models from the Schema

FastAPI uses Pydantic for request and response validation. Define models that reflect the multilingual instruction structure and exercise metadata found in data/exercises.json. This ensures automatic documentation generation and runtime validation.

from pydantic import BaseModel
from typing import List

class Instructions(BaseModel):
    en: str
    es: str
    it: str
    tr: str
    ru: str
    zh: str
    hi: str
    pl: str
    ko: str
    fr: str

class Exercise(BaseModel):
    id: str
    name: str
    category: str
    body_part: str
    equipment: str
    instructions: Instructions
    instruction_steps: dict
    muscle_group: str
    secondary_muscles: List[str]
    target: str
    media_id: str
    image: str
    gif_url: str
    attribution: str
    created_at: str

Loading the Dataset and Initializing the App

Load the JSON data once at startup to avoid file I/O on every request. In app/main.py, read data/exercises.json into a global list, then initialize the FastAPI application and mount static directories for media delivery.

from fastapi import FastAPI, HTTPException, Query
from fastapi.staticfiles import StaticFiles
from pathlib import Path
import json

# Load dataset once at startup

DATA_FILE = Path(__file__).parent.parent / "data" / "exercises.json"
with DATA_FILE.open(encoding="utf-8") as f:
    exercises = json.load(f)

app = FastAPI(title="Exercises API")

# Serve static media files

app.mount("/images", StaticFiles(directory=Path(__file__).parent.parent / "images"), name="images")
app.mount("/videos", StaticFiles(directory=Path(__file__).parent.parent / "videos"), name="videos")

Implementing CRUD Endpoints

With the dataset loaded and models defined, expose RESTful endpoints for listing, retrieving, and creating exercises. Use FastAPI's Query parameters to implement filtering and pagination without additional libraries.

List and Filter Exercises

The GET /exercises endpoint supports filtering by category and equipment, with limit and offset for pagination:

@app.get("/exercises", response_model=list[Exercise])
def list_exercises(
    category: str | None = Query(default=None),
    equipment: str | None = Query(default=None),
    limit: int = Query(default=100, ge=1, le=500),
    offset: int = Query(default=0, ge=0),
):
    filtered = exercises
    if category:
        filtered = [e for e in filtered if e["category"] == category]
    if equipment:
        filtered = [e for e in filtered if e["equipment"] == equipment]
    return filtered[offset : offset + limit]

Retrieve Single Exercise

Access individual records by their unique identifier:

@app.get("/exercises/{exercise_id}", response_model=Exercise)
def get_exercise(exercise_id: str):
    for ex in exercises:
        if ex["id"] == exercise_id:
            return ex
    raise HTTPException(status_code=404, detail="Exercise not found")

Create New Exercise

Validate incoming data against the Exercise model and check for duplicate IDs before appending to the dataset:

@app.post("/exercises", response_model=Exercise, status_code=201)
def create_exercise(payload: Exercise):
    if any(e["id"] == payload.id for e in exercises):
        raise HTTPException(status_code=400, detail="ID already exists")
    exercises.append(payload.dict())
    return payload

Serving Static Media Files

The image and gif_url fields in data/exercises.json contain relative filenames rather than full URLs. By mounting the images/ and videos/ directories at /images and /videos respectively, clients can request media assets directly:

  • Thumbnail: GET http://localhost:8000/images/0001-2gPfomN.jpg
  • Animation: GET http://localhost:8000/videos/0001-2gPfomN.gif

This approach keeps the API self-contained, with all assets served through the same ASGI server without external CDN dependencies.

Running and Testing the API

Start the development server using Uvicorn:

uvicorn app.main:app --reload

The API documentation automatically generates at http://localhost:8000/docs. Test the endpoints with these example calls:

  1. Filter by category: GET /exercises?category=chest&limit=20
  2. Filter by equipment: GET /exercises?equipment=barbell
  3. Retrieve specific record: GET /exercises/0001
  4. Access media: GET /images/0001-2gPfomN.jpg

For production deployment, containerize the application with Docker, ensuring the data/, images/, and videos/ directories are included in the container image or mounted as volumes.

Summary

  • Load once: Read data/exercises.json at startup to minimize disk I/O and leverage FastAPI's async capabilities for concurrent request handling.
  • Validate strictly: Use Pydantic models mirroring data/exercises.schema.json to ensure all requests and responses conform to the expected structure.
  • Mount media: Serve the images/ and videos/ directories via StaticFiles to expose thumbnails and GIFs at predictable URLs.
  • Filter efficiently: Implement query parameter filtering using Python list comprehensions for datasets of this size (1,324 records), or migrate to a database for larger volumes.
  • Reference setup.html: The repository's setup.html provides additional backend integration patterns and client-side testing utilities.

Frequently Asked Questions

How do I validate new exercises against the official schema?

FastAPI automatically validates incoming JSON against your Pydantic Exercise model. For additional validation against the raw data/exercises.schema.json file, use the jsonschema library in your create_exercise endpoint before appending to the dataset.

Can I filter exercises by multiple criteria simultaneously?

Yes. Extend the list_exercises function to accept additional Query parameters such as target, body_part, or muscle_group, then chain multiple list comprehensions or use a single loop with compound conditional logic to narrow results.

How do I handle the multilingual instruction fields?

The Instructions Pydantic model defines each supported language (en, es, it, tr, ru, zh, hi, pl, ko, fr) as required strings. When creating exercises, ensure all language fields are populated, or modify the model to use Optional[str] if partial translations are acceptable for your use case.

Is this implementation suitable for production workloads?

For high-traffic production environments, migrate the in-memory list to a proper database such as PostgreSQL or MongoDB. The current implementation suits prototyping, small deployments, or read-heavy workloads where the 1,324 records fit comfortably in memory.

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 →