A simple place to collect exercises and build reusable workouts.
RepShelf is a small full-stack web app built with vanilla JavaScript and Express. Add exercises with optional notes and images, choose the exercises for a workout, and keep your collection between server restarts using a local JSON file.
The app focuses on planning workouts. It does not track completed sessions, repetitions, weights, or progress.
- Create exercises with a name, an optional description of up to 350 characters, and an optional HTTP(S) image URL.
- Build named workouts by selecting exercises from your collection.
- Edit exercise details and change a workout's name or selected exercises using the existing forms.
- Filter the workout builder's exercise list by the beginning of an exercise name, ignoring letter case.
- Browse exercise cards and workouts with exercise names, descriptions, and images.
- See exercise and workout counts, and refresh the displayed data.
- Confirm deletion before removing an exercise or workout.
- Automatically remove a deleted exercise from every workout that references it.
- Save changes to a local JSON file, with no database service required.
Use Node.js 22 and npm. Local verification uses Node.js 22.20.0 and npm 11.6.4.
Clone the repository and install the locked dependencies:
git clone https://github.com/CutGrass88/repshelf.git
cd repshelf
npm ciCreate your local data file from the included examples. This is required before starting the app; tests create their own isolated fixtures.
Windows PowerShell:
Copy-Item data.example.json data.jsonmacOS or Linux:
cp data.example.json data.jsonRun the server:
npm startOpen http://localhost:3000. Stop the server with Ctrl+C.
Run the npm commands from the repository root. Opening public/index.html directly does not start the API.
The page loads Bootstrap and SweetAlert2 from a CDN. Internet access is needed for those resources and for any remote exercise images.
- Add an exercise in Create exercise. Only its name is required.
- Enter a workout name and select one or more exercises in Create workout.
- Submit the workout to see its exercise cards in the workouts list.
- Use Refresh to fetch the latest data from the server.
- Choose Edit on an exercise or workout to load it into its form. Make your changes and select Save changes, or Cancel to return to creating a new item.
- Use a delete button and confirm the dialog to remove an item.
Selections are retained when you search for another exercise, so a workout can include exercises chosen across different search results.
Editing an exercise keeps its ID, so its updated details appear in workouts that already use it. Editing a workout lets you rename it or change the selected exercises. Cancel does not save changes, and a failed save leaves your entered values in the form so you can retry.
Deleting an exercise also removes it from existing workouts. A workout can remain in the list with no exercises after its last exercise is deleted; select at least one exercise when saving an edit to that workout.
| Part | Implementation |
|---|---|
| Frontend | HTML, CSS, vanilla JavaScript, Fetch API |
| Styling | Bootstrap 4.1.3 from jsDelivr, plus local CSS |
| Confirmation dialogs | SweetAlert2 11 from jsDelivr |
| Server | Node.js, Express 5, CommonJS modules |
| Storage | Local data.json, read into memory at startup |
| Tests | Jest 30 and Supertest 7 |
| Dependency installation | npm with a committed package-lock.json |
There is no frontend build step. Express serves the page and the JSON API from the same server.
flowchart LR
Browser[Browser: HTML, CSS, JavaScript]
Server[Express server]
Assets[public/ assets]
State[In-memory exercises and workouts]
File[(Local data.json)]
CDN[Bootstrap and SweetAlert2 CDN]
Browser -->|Page and asset requests| Server
Server --> Assets
Browser -->|Fetch /api requests| Server
Server --> State
File -->|Read on startup| State
State -->|Save after create, edit or delete| File
Browser -->|Styles and dialogs| CDN
server/server.js starts the HTTP listener. server/serverApp.js creates the Express app, serves static files, validates requests, and handles storage. The browser fetches exercises and workouts, stores them in client-side state, and renders the lists.
repshelf/
├── .github/workflows/test.yml # Linux and Windows checks
├── docs/
│ └── api.md # Endpoint reference and request examples
├── public/
│ ├── index.html # Page structure and forms
│ ├── app.js # Fetch requests, rendering, and interactions
│ └── style.css # App-specific styles
├── server/
│ ├── server.js # Server entry point
│ └── serverApp.js # Express routes and JSON persistence
├── tests/
│ ├── client.test.js # Client behavior regression tests
│ └── server.test.js # API tests
├── data.example.json # Committed sample collection
├── data.json # Local runtime data; created during setup, ignored by Git
├── .gitignore
├── LICENSE
├── package.json
├── package-lock.json
└── README.md
The repository keeps application code, tests, documentation, and neutral example data. Installed dependencies and runtime data belong in your local working copy.
| Command | Purpose |
|---|---|
npm ci |
Install dependencies from the lockfile |
npm start |
Start server/server.js |
npm test |
Run the Jest API and client regression suites |
npm test -- --runInBand |
Run the suite in one process |
| Environment variable | Default | Purpose |
|---|---|---|
PORT |
3000 |
HTTP listener port |
DATA_PATH |
data.json in the app root |
Path to an existing JSON collection; relative overrides resolve from the working directory |
Windows PowerShell:
$env:PORT = "3001"
npm startmacOS or Linux:
PORT=3001 npm startThe app does not load .env files automatically. Set environment variables in your shell or process configuration. If you use DATA_PATH, keep the file outside the repository or explicitly ignore it so personal data is not committed.
All routes are relative to the running server. Send JSON request bodies with Content-Type: application/json.
| Method | Endpoint | Result |
|---|---|---|
GET |
/api/exercises |
List exercises |
GET |
/api/exercises/:id |
Get one exercise |
POST |
/api/exercises |
Create an exercise |
PUT |
/api/exercises/:id |
Update an exercise while keeping its ID |
DELETE |
/api/exercises/:id |
Delete an exercise and remove its workout references |
GET |
/api/workouts |
List workouts with their exercise IDs |
GET |
/api/workouts/:id |
Get a workout with its exercise names resolved |
POST |
/api/workouts |
Create a workout from existing exercise IDs |
PUT |
/api/workouts/:id |
Update a workout's name and selected exercises |
DELETE |
/api/workouts/:id |
Delete a workout |
See the complete API reference for schemas, examples, validation rules, status codes, and current edge cases.
data.example.json includes four exercises—Bench Press, Squat, Row, and Plank—and two workouts: Full Body and Core. Its image fields are empty, so the sample collection does not depend on remote images.
Copying this file creates data.json, which is excluded from version control. The app reads that file once at startup and saves the whole collection after each successful create, edit or delete operation. Its top-level structure is:
{
"exercises": [],
"workouts": [],
"nextExerciseID": 1,
"nextWorkoutID": 1
}Each workout stores an array of exercise IDs. The two counters supply IDs for new records; editing and deletion do not change them. When preparing custom data, keep those counters above the largest existing IDs and ensure workout references point to existing exercises.
Saving first writes a temporary file beside the collection, then replaces the collection file. The in-memory data changes only after this succeeds. If a write or replacement fails, the previous collection and ID counters remain intact, and the API returns a save error. The data directory must be writable as well as the file itself.
To back up your collection, stop the server and copy data.json to a safe location outside the repository. To restore it, stop the server, replace data.json with your backup, and restart. Manual file changes are not picked up until the server restarts.
To reset to the sample collection, stop the server, back up anything you want to keep, and repeat the setup copy command. Replacing data.json replaces your current collection. A missing or malformed JSON file prevents the server from starting.
With dependencies installed, run:
npm test -- --runInBandThe API suite uses Supertest to exercise the Express app without running npm start separately. Each test gets a temporary data fixture and leaves your runtime collection untouched. Coverage includes reads, creation, editing, validation, malformed requests, missing records, deletion, cascading workout updates, and recovery from failed writes and file replacements.
Client regression tests check editing and cancellation, failed saves, workout selections across search results, image fallback, and confirmation behavior using lightweight DOM fixtures. They do not replace testing in a real browser. Mobile layout and visual rendering have not been verified in this environment.
The test workflow runs the suite on Linux and Windows with Node.js 22 and checks npm dependencies for high-severity advisories. To collect local coverage, run npm test -- --runInBand --coverage.
Local verification on 14 September 2026: 109 tests passed (87 API and 22 client tests), with 99.35% line coverage for server/serverApp.js. The VM-based client tests do not contribute browser coverage. npm audit reported zero vulnerabilities; audit results may change as new advisories are published.
- Single shared collection: there are no accounts, authentication, or authorization. Anyone who can reach the server can read, create, edit, or delete data. The listener does not explicitly restrict itself to localhost.
- Simple file storage: each change writes the whole JSON file synchronously. Failed saves preserve the previous collection, but there is no file locking or coordination between server processes. Run one server process against a data file and keep backups.
- External browser resources: styling, enhanced confirmation dialogs, and optional images rely on external hosts. Deletion falls back to the browser's built-in confirmation dialog if SweetAlert2 cannot load.
- Planning only: there is no exercise ordering control, sets/reps tracking, workout history, or progress reporting.
This is a small local application and a full-stack project example. Public deployment would need authentication and more robust storage.
- Add tests in a real browser for keyboard interaction and responsive layouts.
- Replace file storage with a database if the app grows beyond one local process.
- Add user accounts before supporting a shared public service.
Licensed under the ISC License, matching the project's package metadata. Third-party dependencies retain their own licenses.