apisizinghow-toe-commercejavascript

How to Build a Size Recommendation Widget in Vanilla JavaScript

· 5 min read · Martin Hejda

Most size recommendation tutorials assume a specific framework — React, Vue, Next.js. If you’re running a CMS-based storefront, a jQuery site, or any setup where adding a framework isn’t worth the overhead, you still need a working implementation. This guide builds one from scratch using plain HTML, vanilla JavaScript, and a minimal Node.js proxy.

The proxy matters. You cannot call the DimensionsPot API directly from the browser — your API key would be exposed in every network request. All API calls go server-side; the browser only receives the computed result.


Architecture

Browser form
  → POST /api/size (your Node.js server)
      → POST /v1/predict (DimensionsPot API — server-side only)
      ← body_dimensions{}
  ← { recommended_size, chest_mm, waist_mm }
Browser renders result

Two files: a Node.js server (server.js) and the widget HTML (widget.html). You can embed the widget on any page.


Step 1: The server-side proxy

This server handles the API call, caching, input validation, and size chart logic. The browser never sees the API key.

// server.js
const express = require('express');
const app = express();
app.use(express.json());
app.use(express.static('public')); // serves widget.html

const API_URL = 'https://dimensionspot-bodysize-engine.p.rapidapi.com/v1/predict';
const RAPIDAPI_KEY = process.env.RAPIDAPI_KEY;

// Identical inputs always produce identical outputs — safe to cache indefinitely
const cache = new Map();

// Women's tops size chart — chest_circumference in mm
// Replace with your brand's actual measurements
const WOMENS_TOPS = [
  { size: 'XS',  maxChest: 840,  maxWaist: 680 },
  { size: 'S',   maxChest: 880,  maxWaist: 720 },
  { size: 'M',   maxChest: 930,  maxWaist: 760 },
  { size: 'L',   maxChest: 980,  maxWaist: 820 },
  { size: 'XL',  maxChest: 1040, maxWaist: 890 },
  { size: 'XXL', maxChest: Infinity, maxWaist: Infinity },
];

const MENS_TOPS = [
  { size: 'S',   maxChest: 940,  maxWaist: 840 },
  { size: 'M',   maxChest: 990,  maxWaist: 880 },
  { size: 'L',   maxChest: 1060, maxWaist: 940 },
  { size: 'XL',  maxChest: 1130, maxWaist: 1010 },
  { size: 'XXL', maxChest: 1200, maxWaist: 1090 },
  { size: '3XL', maxChest: Infinity, maxWaist: Infinity },
];

function recommendSize(chest, waist, gender) {
  const chart = gender === 'female' ? WOMENS_TOPS : MENS_TOPS;
  // Use the more restrictive fit — avoid anything too tight
  return chart.find(s => chest <= s.maxChest && waist <= s.maxWaist)
    ?? chart[chart.length - 1];
}

app.post('/api/size', async (req, res) => {
  const { gender, height_cm, weight_kg } = req.body;

  // Validate required fields
  if (!gender || !height_cm || !weight_kg) {
    return res.status(400).json({ error: 'gender, height_cm, and weight_kg are required' });
  }
  if (!['male', 'female'].includes(gender)) {
    return res.status(400).json({ error: 'gender must be "male" or "female"' });
  }

  const heightMm = Math.round(parseFloat(height_cm) * 10);
  const weightKg = parseFloat(weight_kg);

  if (isNaN(heightMm) || heightMm < 1200 || heightMm > 2300) {
    return res.status(400).json({ error: 'Height must be between 120 and 230 cm' });
  }
  if (isNaN(weightKg) || weightKg < 30 || weightKg > 250) {
    return res.status(400).json({ error: 'Weight must be between 30 and 250 kg' });
  }

  // Cache key: inputs fully determine outputs (stateless API)
  const cacheKey = `${gender}-${heightMm}-${weightKg}`;

  if (!cache.has(cacheKey)) {
    try {
      const apiResponse = 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: 'GLOBAL' },
            anchors: { body_height: heightMm, body_mass: weightKg },
          },
          output_settings: {
            calculation: {
              calculation_model: 'AUTO',
              target_region: 'GLOBAL',
              body_build_type: 'CIVILIAN',
            },
            requested_dimensions: { bundle: 'TORSO' },
            output_format: {
              unit_system: 'metric',
              confidence_score_threshold: 0,
              include_range_95: false,
              include_iso_codes: false,
            },
          },
        }),
      });

      if (!apiResponse.ok) {
        return res.status(502).json({ error: 'Prediction service unavailable' });
      }

      cache.set(cacheKey, await apiResponse.json());
    } catch {
      return res.status(502).json({ error: 'Could not reach prediction service' });
    }
  }

  const apiData = cache.get(cacheKey);

  // Build a keyed dimension map from the response object
  const dims = Object.fromEntries(
    Object.entries(apiData.body_dimensions).map(([key, d]) => [key, d.value])
  );

  const chest = dims.chest_circumference ?? 0;
  const waist = dims.waist_circumference_natural ?? 0;
  const { size } = recommendSize(chest, waist, gender);

  res.json({
    recommended_size: size,
    chest_mm: chest,
    waist_mm: waist,
  });
});

app.listen(3000, () => console.log('Size server running on port 3000'));

Step 2: The widget HTML

Drop this in any page. It calls your proxy, not the API directly.

<!-- public/widget.html -->
<!doctype html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <title>Find Your Size</title>
  <style>
    .size-widget { font-family: sans-serif; max-width: 320px; padding: 1.5rem;
                   border: 1px solid #e0e0e0; border-radius: 8px; }
    .size-widget label { display: block; margin-bottom: 0.25rem; font-size: 0.875rem; color: #555; }
    .size-widget select,
    .size-widget input  { width: 100%; padding: 0.5rem; margin-bottom: 1rem;
                          border: 1px solid #ccc; border-radius: 4px; font-size: 1rem; box-sizing: border-box; }
    .size-widget button { width: 100%; padding: 0.75rem; background: #111; color: #fff;
                          border: none; border-radius: 4px; font-size: 1rem; cursor: pointer; }
    .size-widget button:disabled { opacity: 0.5; cursor: not-allowed; }
    #size-result { margin-top: 1.25rem; padding: 1rem; background: #f5f5f5;
                   border-radius: 6px; text-align: center; display: none; }
    #size-result strong { font-size: 2rem; display: block; margin-bottom: 0.25rem; }
    #size-result small  { color: #777; font-size: 0.8rem; }
    #size-error  { margin-top: 1rem; color: #c00; font-size: 0.875rem; display: none; }
  </style>
</head>
<body>
<div class="size-widget">
  <h2 style="margin-top:0">Find your size</h2>
  <form id="size-form">
    <label for="gender">Gender</label>
    <select id="gender" name="gender" required>
      <option value="">Select…</option>
      <option value="female">Female</option>
      <option value="male">Male</option>
    </select>

    <label for="height_cm">Height (cm)</label>
    <input id="height_cm" name="height_cm" type="number" min="120" max="230"
           step="0.5" placeholder="e.g. 168" required>

    <label for="weight_kg">Weight (kg)</label>
    <input id="weight_kg" name="weight_kg" type="number" min="30" max="250"
           step="0.5" placeholder="e.g. 65" required>

    <button type="submit" id="submit-btn">Calculate size</button>
  </form>

  <div id="size-result">
    <small>Recommended size</small>
    <strong id="size-label"></strong>
    <small id="measurements"></small>
  </div>
  <p id="size-error"></p>
</div>

<script>
document.getElementById('size-form').addEventListener('submit', async function(e) {
  e.preventDefault();

  const btn = document.getElementById('submit-btn');
  btn.disabled = true;
  btn.textContent = 'Calculating…';
  document.getElementById('size-result').style.display = 'none';
  document.getElementById('size-error').style.display = 'none';

  const data = {
    gender:    document.getElementById('gender').value,
    height_cm: document.getElementById('height_cm').value,
    weight_kg: document.getElementById('weight_kg').value,
  };

  try {
    const res = await fetch('/api/size', {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify(data),
    });

    const json = await res.json();

    if (!res.ok) {
      throw new Error(json.error ?? 'Unknown error');
    }

    document.getElementById('size-label').textContent = json.recommended_size;
    document.getElementById('measurements').textContent =
      `Chest ≈ ${(json.chest_mm / 10).toFixed(0)} cm · Waist ≈ ${(json.waist_mm / 10).toFixed(0)} cm`;
    document.getElementById('size-result').style.display = 'block';

  } catch (err) {
    const errEl = document.getElementById('size-error');
    errEl.textContent = err.message ?? 'Could not calculate size. Please try again.';
    errEl.style.display = 'block';
  } finally {
    btn.disabled = false;
    btn.textContent = 'Calculate size';
  }
});
</script>
</body>
</html>

Step 3: Replacing the size chart

The WOMENS_TOPS and MENS_TOPS arrays in the server are placeholders. Replace them with your brand’s actual measurements, measured at the garment, not the body. A good process:

  1. Measure the chest circumference and waist circumference of each physical size sample at the widest point.
  2. Add 4–6 cm ease to each for a regular fit (this is already “built in” to most garments, but confirm it for yours).
  3. The resulting numbers are your maxChest and maxWaist thresholds.

The API returns body dimensions, not garment dimensions. The size chart is the bridge between the two. Getting this right matters more than the API integration itself.


Adding inches/centimetres toggle

If your users think in inches, convert on the client before sending:

function toCm(value, unit) {
  return unit === 'in' ? parseFloat(value) * 2.54 : parseFloat(value);
}

// In the submit handler:
const height_cm = toCm(heightInput.value, selectedUnit);
const weight_kg = weightUnit === 'lbs' ? parseFloat(weightInput.value) * 0.453592 : parseFloat(weightInput.value);

The server always receives metric. Keep unit conversion on the client side where the user interacts with it.


Deploying

The server runs anywhere Node.js runs — a VPS, Railway, Render, or a serverless function. The widget HTML is a static file served by the same server or from a CDN. For serverless, replace the Express handler with a function handler (Vercel, Netlify, AWS Lambda all work identically for this pattern — just move the app.post logic into the function).

Set RAPIDAPI_KEY as an environment variable. Never hardcode it.

Try DimensionsPot

Free tier — 100 requests/month, no credit card required.

Get API on RapidAPI