Sports equipment sizing is harder than clothing sizing for one reason: the consequences of getting it wrong are immediate and physical. A poorly fitted bicycle saddle causes knee injury over time. A wetsuit that’s too short pulls at the shoulders every stroke. A ski that’s 20 cm too long is actively dangerous for an intermediate skier.
Most sports equipment brands publish size charts. The problem is that these charts use height as the primary — often only — variable, when body proportions matter significantly. A 180 cm cyclist with short legs and a long torso needs a different frame than a 180 cm cyclist with the opposite proportions. Height alone doesn’t capture this.
This guide builds a multi-sport recommender that covers road bicycles, wetsuits, skis, and helmets — four categories where body dimensions beyond height genuinely improve recommendation accuracy.
Architecture
One server endpoint, one bundle request per user, four independent recommendation functions. The FULL_BODY bundle returns all dimensions needed across all four sports, so a single API call supports the entire recommender.
Step 1: Server endpoint
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;
const cache = new Map();
// ── Bicycle frame sizing (road/gravel) ──────────────────────────────────────
// Based on inseam length as primary, arm_length + torso_length for reach
function bikeSizing(dims) {
const inseam = dims.inseam_length ?? 0; // mm
const torso = dims.torso_length ?? 0; // mm (shoulder to hip)
const arm = dims.arm_length ?? 0; // mm
// Standover height clearance: inseam - 30mm minimum
const standoverClearance = inseam - 30;
// Saddle height (centre of BB to top of saddle): LeMond formula
const saddleHeightMm = Math.round(inseam * 0.885);
// Road bike frame size (seat tube, cm) — standard sizing formula
const frameSize = inseam / 10 * 0.65;
const FRAME_SIZES = [
{ label: 'XS', maxFrame: 48 },
{ label: 'S', maxFrame: 52 },
{ label: 'M', maxFrame: 54 },
{ label: 'L', maxFrame: 56 },
{ label: 'XL', maxFrame: 58 },
{ label: 'XXL', maxFrame: Infinity },
];
const recommended = FRAME_SIZES.find(s => frameSize <= s.maxFrame) ?? FRAME_SIZES[FRAME_SIZES.length - 1];
return {
frame_size_label: recommended.label,
frame_size_cm: Math.round(frameSize * 10) / 10,
saddle_height_mm: saddleHeightMm,
reach_note: torso && arm
? `Long reach preference suggested: ${Math.round((torso + arm) / 10)} cm combined torso+arm.`
: null,
};
}
// ── Wetsuit sizing ──────────────────────────────────────────────────────────
// Primary: height + chest. Secondary: weight band for buoyancy/compression.
function wetsuitSizing(dims, weightKg) {
const height = dims.body_height ?? 0; // mm
const chest = dims.chest_circumference ?? 0; // mm
// Most major brands (O'Neill, Rip Curl, Patagonia) follow similar sizing
const WETSUIT_SIZES = [
{ label: 'XS', maxHeight: 1680, maxChest: 880, maxWeight: 60 },
{ label: 'S', maxHeight: 1750, maxChest: 930, maxWeight: 72 },
{ label: 'MS', maxHeight: 1780, maxChest: 950, maxWeight: 78 },
{ label: 'M', maxHeight: 1800, maxChest: 980, maxWeight: 84 },
{ label: 'ML', maxHeight: 1830, maxChest: 1010, maxWeight: 91 },
{ label: 'L', maxHeight: 1860, maxChest: 1050, maxWeight: 98 },
{ label: 'XL', maxHeight: 1900, maxChest: 1090, maxWeight: 108 },
{ label: 'XXL', maxHeight: 1940, maxChest: 1130, maxWeight: 120 },
{ label: 'XXXL', maxHeight: Infinity, maxChest: Infinity, maxWeight: Infinity },
];
// Use chest as primary fit dimension; height and weight as secondary checks
const byChest = WETSUIT_SIZES.filter(s => chest <= s.maxChest);
const byAll = byChest.find(s => height <= s.maxHeight && weightKg <= s.maxWeight);
const size = byAll ?? byChest[byChest.length - 1] ?? WETSUIT_SIZES[WETSUIT_SIZES.length - 1];
return {
recommended_size: size.label,
note: 'Wetsuits should feel snug without restricting arm movement or breathing. '
+ 'If between sizes, size up for comfort; size down for performance (triathlon).',
};
}
// ── Ski sizing ──────────────────────────────────────────────────────────────
function skiSizing(dims, weightKg, skillLevel = 'intermediate') {
const heightCm = (dims.body_height ?? 0) / 10;
// Skill-based offset: beginner −15 to −20 cm, intermediate −5 to −10 cm,
// advanced 0 to +5 cm, expert +5 to +10 cm
const OFFSETS = {
beginner: -17,
intermediate: -7,
advanced: 0,
expert: 7,
};
// Weight correction: heavier skiers benefit from slightly longer skis
const weightCorrection = weightKg > 90 ? 5 : weightKg < 60 ? -5 : 0;
const idealLength = Math.round(heightCm + (OFFSETS[skillLevel] ?? -7) + weightCorrection);
// Helmet size from head circumference
const headCircCm = (dims.head_circumference ?? 560) / 10;
const HELMET_SIZES = [
{ label: 'XS', maxCirc: 53 },
{ label: 'S', maxCirc: 55 },
{ label: 'M', maxCirc: 57 },
{ label: 'L', maxCirc: 59 },
{ label: 'XL', maxCirc: 61 },
{ label: 'XXL', maxCirc: Infinity },
];
const helmet = HELMET_SIZES.find(s => headCircCm <= s.maxCirc) ?? HELMET_SIZES[HELMET_SIZES.length - 1];
return {
ski_length_cm: idealLength,
skill_level: skillLevel,
helmet_size: helmet.label,
head_circ_cm: Math.round(headCircCm * 10) / 10,
boot_size_eu: Math.round((dims.foot_length ?? 265) / 6.667), // Mondopoint → EU approx
note: `Ski length ${idealLength} cm ±5 cm. Intermediate skiers: err on shorter for easier control.`,
};
}
// ── Main endpoint ────────────────────────────────────────────────────────────
router.post('/sports', async (req, res) => {
const { gender, height_cm, weight_kg, sport, skill_level, region } = req.body;
if (!gender || !height_cm || !weight_kg || !sport) {
return res.status(400).json({ error: 'gender, height_cm, weight_kg, and sport are required' });
}
const heightMm = Math.round(parseFloat(height_cm) * 10);
const weightKg = parseFloat(weight_kg);
const targetRegion = region ?? 'GLOBAL';
const cacheKey = `${gender}-${heightMm}-${weightKg}-${targetRegion}`;
if (!cache.has(cacheKey)) {
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: targetRegion },
anchors: { body_height: heightMm, body_mass: weightKg },
},
output_settings: {
calculation: { calculation_model: 'AUTO', target_region: targetRegion, 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: 'Prediction service unavailable' });
const data = await apiRes.json();
cache.set(cacheKey, Object.fromEntries(
Object.entries(data.body_dimensions).map(([key, d]) => [key, d.value])
));
}
const dims = cache.get(cacheKey);
const sportHandlers = {
bike: () => bikeSizing(dims),
wetsuit: () => wetsuitSizing(dims, weightKg),
ski: () => skiSizing(dims, weightKg, skill_level ?? 'intermediate'),
};
const handler = sportHandlers[sport.toLowerCase()];
if (!handler) {
return res.status(400).json({
error: `Unknown sport "${sport}". Supported: bike, wetsuit, ski`,
});
}
res.json({ sport, ...handler() });
});
module.exports = router;
Step 2: Client integration
A minimal sport selector form:
<form id="sports-form">
<select name="gender" required>
<option value="male">Male</option>
<option value="female">Female</option>
</select>
<input name="height_cm" type="number" placeholder="Height (cm)" required>
<input name="weight_kg" type="number" placeholder="Weight (kg)" required>
<select name="sport" required>
<option value="bike">Road / Gravel bike</option>
<option value="wetsuit">Wetsuit</option>
<option value="ski">Ski / Snowboard</option>
</select>
<select name="skill_level">
<option value="beginner">Beginner</option>
<option value="intermediate" selected>Intermediate</option>
<option value="advanced">Advanced</option>
<option value="expert">Expert / Racing</option>
</select>
<button type="submit">Find my size</button>
</form>
<div id="result" hidden></div>
<script>
document.getElementById('sports-form').addEventListener('submit', async e => {
e.preventDefault();
const data = Object.fromEntries(new FormData(e.target));
const res = await fetch('/api/sports', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(data),
});
const result = await res.json();
document.getElementById('result').hidden = false;
document.getElementById('result').innerHTML =
`<pre>${JSON.stringify(result, null, 2)}</pre>`;
});
</script>
Accuracy notes by sport
Bicycle sizing: Inseam-derived saddle height is established and accurate to within 10–15 mm for most riders. Frame size from inseam is reliable; the reach dimension (handlebar distance) involves torso length, which the API estimates rather than measures directly. For competitive cyclists, a professional bike fit remains the right answer.
Wetsuit sizing: Chest circumference from the API is the most impactful input for wetsuit fit. The chest dimension has high confidence scores for most height/weight combinations. Torso length (for fit of the shoulders and crotch panel) is predicted, so tall people with short torsos or vice versa may need to verify with the brand’s return policy.
Ski sizing: Height-minus-offset formulas are the industry standard for recreational skiers. Weight correction is a commonly used refinement. For racing, shaped carving skis, or expert-level technique, these formulas are a starting point — an in-store expert should confirm.
Helmets (all sports): Head circumference has high prediction confidence. For helmets, always recommend that customers measure their own head circumference to confirm — a €300 helmet is worth verifying before purchase.