Fitness apps collect height and weight at signup. Almost all of them use those two numbers for exactly one purpose: BMI. Then the numbers sit in the database, unused, while the app serves the same generic workouts to a 155 cm person and a 195 cm person.
Height and weight are enough to derive body proportions that meaningfully affect exercise performance, form, and equipment needs. Stride length, shoulder width, arm reach, torso height — these determine how far each step covers, how wide your swimming stroke should be, whether your bike saddle adjustment is correct, how to set up a barbell safely.
This guide builds an onboarding enrichment layer: call DimensionsPot at signup (or first launch, if height/weight were already collected), store the relevant proportions alongside the user profile, and use them to personalise the app experience from day one.
What body proportions inform in a fitness context
| Dimension | Fitness application |
|---|---|
inseam_length | Stride length estimation, cycling saddle height, stair climbing step rate |
arm_length | Swimming stroke reach, barbell grip width, boxing reach |
shoulder_width | Swimming stroke width, rowing catch width, deadlift stance |
torso_length | Squat depth, back extension reach, swimming body rotation |
foot_length | Running shoe fit, deadlift stance width |
body_height | Platform heights, pull-up bar clearance |
None of these require anything beyond height and weight from the user. The app already has that data.
Step 1: Enrichment endpoint
Call this endpoint once, at account creation or first login. Store the result — don’t recalculate on every request.
// fitness-onboarding.js
const express = require('express');
const router = express.Router();
const API_URL = 'https://dimensionspot-bodysize-engine.p.rapidapi.com/v1/predict';
const RAPIDAPI_KEY = process.env.RAPIDAPI_KEY;
// Which dimensions matter for fitness — request FULL_BODY and extract
const FITNESS_DIMENSIONS = [
'body_height',
'inseam_length',
'arm_length',
'shoulder_width',
'torso_length',
'foot_length',
'foot_breadth',
'hand_length',
'hand_breadth',
'neck_circumference',
'chest_circumference',
'waist_circumference_natural',
'hip_circumference',
'thigh_circumference',
'calf_circumference',
];
router.post('/enrich-profile', async (req, res) => {
const { userId, gender, height_cm, weight_kg, region } = req.body;
if (!userId || !gender || !height_cm || !weight_kg) {
return res.status(400).json({ error: 'userId, gender, height_cm, and weight_kg are required' });
}
const heightMm = Math.round(parseFloat(height_cm) * 10);
const weightKg = parseFloat(weight_kg);
const bodyRegion = region ?? 'GLOBAL';
const apiRes = await fetch(API_URL, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'x-rapidapi-key': RAPIDAPI_KEY,
'x-rapidapi-host': 'dimensionspot-bodysize-engine.p.rapidapi.com',
},
body: JSON.stringify({
input_data: {
input_unit_system: 'metric',
subject: { gender, input_origin_region: bodyRegion },
anchors: { body_height: heightMm, body_mass: weightKg },
},
output_settings: {
calculation: { calculation_model: 'AUTO', target_region: bodyRegion, body_build_type: 'CIVILIAN' },
requested_dimensions: { bundle: 'FULL_BODY' },
output_format: {
unit_system: 'metric',
confidence_score_threshold: 0,
include_range_95: false,
include_iso_codes: false,
},
},
}),
});
if (!apiRes.ok) return res.status(502).json({ error: 'Body dimension service unavailable' });
const data = await apiRes.json();
// Extract only the fitness-relevant dimensions
const proportions = {};
for (const [key, d] of Object.entries(data.body_dimensions)) {
if (FITNESS_DIMENSIONS.includes(key)) {
proportions[key] = {
value: d.value,
confidence: d.confidence_score,
};
}
}
// Derive computed metrics useful for fitness
const inseam = proportions.inseam_length?.value ?? 0;
const arm = proportions.arm_length?.value ?? 0;
const derived = {
// Walking stride length estimate: inseam × 1.35 (average step × 2)
estimated_stride_length_cm: inseam ? Math.round(inseam * 1.35 / 10) : null,
// Cycling saddle height: inseam × 0.885
cycling_saddle_height_mm: inseam ? Math.round(inseam * 0.885) : null,
// Swimming catch width estimate: shoulder_width + arm_length × 0.4
swimming_catch_width_cm: arm
? Math.round(((proportions.shoulder_width?.value ?? 0) + arm * 0.4) / 10)
: null,
};
// Store proportions in user profile — do NOT store height/weight here if privacy-sensitive
// await db.users.update(userId, { bodyProportions: proportions, derivedMetrics: derived });
res.json({ proportions, derived });
});
module.exports = router;
Step 2: Using proportions in workout personalisation
Once proportions are stored in the user profile, use them to adjust workout parameters:
// workoutPersonalizer.js
function personalizeWorkout(workout, userProportions) {
const inseam = userProportions.inseam_length?.value ?? 790;
const arm = userProportions.arm_length?.value ?? 590;
const shoulder = userProportions.shoulder_width?.value ?? 410;
const adjustments = {};
// Running: step target based on stride length
if (workout.type === 'running') {
const strideCm = Math.round(inseam * 1.35 / 10);
// Steps per km = 100,000 / stride_cm
adjustments.steps_per_km_estimate = Math.round(100000 / strideCm);
adjustments.cadence_note = strideCm < 75
? 'Short stride: aim for higher cadence (180+ spm) to maintain pace.'
: strideCm > 90
? 'Long stride: focus on not overstriding — land under your hips, not ahead.'
: null;
}
// Swimming: lane and stroke guidance
if (workout.type === 'swimming') {
const catchWidthCm = Math.round((shoulder + arm * 0.4) / 10);
adjustments.catch_width_cm = catchWidthCm;
adjustments.stroke_note = catchWidthCm > 75
? 'Wide reach: use full extension on each stroke for maximum efficiency.'
: 'Compact reach: focus on rotation to extend effective stroke length.';
}
// Strength: equipment setup guidance
if (workout.type === 'strength') {
const armCm = arm / 10;
const shoulderCm = shoulder / 10;
if (workout.exercise === 'deadlift') {
adjustments.grip_width_note = shoulder > 420
? `Wide grip (${Math.round(shoulderCm + 15)} cm): use wider stance to avoid torso collision.`
: `Standard grip (${Math.round(shoulderCm + 5)} cm).`;
}
if (workout.exercise === 'bench_press') {
adjustments.grip_width_cm = Math.round(shoulderCm * 1.5);
adjustments.grip_note = `Standard: ${Math.round(shoulderCm * 1.5)} cm. Wide grip reduces pec ROM for shorter arms.`;
}
}
return { ...workout, personalisation: adjustments };
}
Step 3: Progressive disclosure in the UI
Don’t show the personalisation calculations to users as raw numbers — frame them as insights:
✓ Your stride: approximately 85 cm
Target cadence for a 5:30/km pace: ~175 steps/min
✓ Cycling setup
Recommended saddle height: 75 cm (from pedal centre to top of saddle)
✓ Barbell bench press
Grip width for your shoulder width: ~68 cm
Users don’t need to know that these come from a body measurement API. They see a personalised experience from day one, without having been asked anything beyond height and weight.
What to store and what to discard
Store the derived proportions (the proportions and derived objects returned by the endpoint), not the height and weight inputs. The proportions contain the information the app needs; the inputs are a transient means to compute them.
If the user updates their weight — after a significant change, or after a strength programme — recompute the proportions. The recomputation costs one API call and updates the stored values. Height rarely changes for adult users; an annual re-check is sufficient.
Privacy note
Body proportions derived from height and weight are not biometric data in the GDPR/HIPAA sense — they’re population-level predictions, not measured biometric samples. Storing inseam_length: 790mm in a user profile is equivalent to storing “approximately medium build” — meaningful for personalisation, not identifiable on its own. This is a substantially lower risk profile than storing the height and weight that generated it, which are closer to direct personal data in the fitness context.