Simple Self‑Hosted Workout Tracker Clone

A minimal openGym‑inspired app in 45 minutes

Welcome to another official UVF IT guide! In this build you’ll create a tiny self‑hosted workout tracker that mimics the core of openGym – plan a week, log sets and weight, all running locally.

The stack is plain Node.js with Express and SQLite, completely free‑tier and perfect for a first‑time developer.

Before you start

Architecture Overview
LOCAL STACK reads/writes HTTP requests Express API + HTML UI SQLite DB file Browser UI
1

Phase 1: Scaffold the project

First we create a clean folder for the clone, turn it into an npm project, and pull in the two runtime dependencies we need: Express for the web server and better‑sqlite3 for a fast, zero‑config SQLite wrapper. All of this fits comfortably under 25 lines of terminal commands.

The npm init -y step gives us a ready‑made package.json so we can later add a start script and run the app with node ..

mkdir openGymClone
cd openGymClone
npm init -y
npm install express better-sqlite3
# optional: add a start script
npm set-script start "node ."
2

Phase 2: Data model – workouts & weight

With the server skeleton ready we need a tiny SQLite database to hold workout plans and weight logs. We'll create an init_db.js script that runs once, creates two tables, and then exits. Running it populates a workout.db file in the project root.

The workouts table stores a name and optional description, while the logs table records a workout reference, date, sets, reps, and weight – exactly what the original openGym UI expects for a minimal clone.

init_db.js

import Database from 'better-sqlite3';
const db = new Database('workout.db');
// Create workouts table
db.exec(`
CREATE TABLE IF NOT EXISTS workouts (
  id INTEGER PRIMARY KEY AUTOINCREMENT,
  name TEXT NOT NULL,
  description TEXT
);
`);
// Create logs table – each entry links to a workout
db.exec(`
CREATE TABLE IF NOT EXISTS logs (
  id INTEGER PRIMARY KEY AUTOINCREMENT,
  workout_id INTEGER NOT NULL,
  date TEXT NOT NULL,
  sets INTEGER,
  reps INTEGER,
  weight REAL,
  FOREIGN KEY(workout_id) REFERENCES workouts(id)
);
`);
console.log('Database initialized');
process.exit(0);
3

Phase 3: Simple API & UI

We’ll spin up a tiny Express server that talks to a JSON file acting as our database. The API will expose two routes: one to fetch all workout logs and another to add a new entry. To make the clone usable without a separate front‑end, we also serve a minimal HTML page with a form that posts to the add‑log endpoint.

The HTML page lives right inside the server so you can open http://localhost:3000 in any browser and start logging workouts instantly. All the code fits in a single file, keeping the project easy to understand and extend.

index.js

const express = require('express');
const path = require('path');
const { Low, JSONFile } = require('lowdb');

const app = express();
app.use(express.json());
app.use(express.urlencoded({ extended: true }));

// ---------- LowDB setup ----------
const file = path.join(__dirname, 'db.json');
const adapter = new JSONFile(file);
const db = new Low(adapter);
(async () => {
  await db.read();
  db.data ||= { workouts: [] };
  await db.write();
})();

// ---------- API ----------
app.get('/workouts', async (req, res) => {
  await db.read();
  res.json(db.data.workouts);
});

app.post('/workouts', async (req, res) => {
  const { name, reps, weight } = req.body;
  const entry = { id: Date.now(), name, reps: Number(reps), weight: Number(weight) };
  await db.read();
  db.data.workouts.push(entry);
  await db.write();
  res.redirect('/');
});

// ---------- UI ----------
app.get('/', (req, res) => {
  res.send(`
    <!doctype html>
    <html><head><title>UVF Workout Tracker</title></head><body>
      <h1>Log a Workout</h1>
      <form method="POST" action="/workouts">
        <input name="name" placeholder="Exercise" required />
        <input name="reps" placeholder="Reps" type="number" required />
        <input name="weight" placeholder="Weight (kg)" type="number" required />
        <button type="submit">Add</button>
      </form>
      <h2>Existing Logs</h2>
      <ul id="list"></ul>
      <script>
        fetch('/workouts').then(r=>r.json()).then(data=>{
          const ul=document.getElementById('list');
          data.forEach(i=>{ const li=document.createElement('li'); li.textContent=`${i.name}: ${i.reps} reps @ ${i.weight}kg`; ul.appendChild(li); });
        });
      </script>
    </body></html>
  `);
});

const PORT = process.env.PORT||3000;
app.listen(PORT,()=>console.log('🚀 http://localhost:'+PORT));
4

Phase 4: Run locally

Before you start the server, make sure the JSON database file exists – the first run will create it automatically. Then launch the app with Node; it will listen on port 3000 and print a friendly URL so you know it’s alive.

You can stop the server with Ctrl+C and restart it anytime; the stored workouts persist between runs because they live in db.json.

npm init -y
npm install express lowdb
node index.js
5

Phase 5: Verify & extend

Open your browser to http://localhost:3000 and you should see the simple form and any previously logged workouts. Fill out the form, submit, and watch the list update instantly – that proves the UI talks to the API correctly.

To double‑check the data layer, query the endpoint with curl or inspect db.json directly. From here you can add features like user authentication, more exercise fields, or a chart view.

# Verify the API returns JSON (should be an array, possibly empty)
curl http://localhost:3000/workouts

# Add a workout via curl (mirrors the form submission)
curl -X POST http://localhost:3000/workouts -d "name=Pushup&reps=20&weight=0"

# Re‑fetch to see the new entry
curl http://localhost:3000/workouts

Verifying the Stack

CheckCommand / Why it matters
Server starts without errorsnode index.js shows '🚀 http://localhost:3000'
/workouts returns JSON arraycurl http://localhost:3000/workouts
UI loads in browserVisit http://localhost:3000 and see the form

You did it!

You now have a functional, self‑hosted workout tracker. From here you can add authentication, a weekly planner UI, or sync it across devices – the sky’s the limit.