<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Tosh Koevoets</title>
    <description>The latest articles on DEV Community by Tosh Koevoets (@tosh_koevoets_c167a1efc0b).</description>
    <link>https://dev.to/tosh_koevoets_c167a1efc0b</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F2237043%2F302fe11d-67d4-41f0-b9d2-027265dfd7d7.png</url>
      <title>DEV Community: Tosh Koevoets</title>
      <link>https://dev.to/tosh_koevoets_c167a1efc0b</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/tosh_koevoets_c167a1efc0b"/>
    <language>en</language>
    <item>
      <title>8 exercise APIs compared: ExerciseDB, MuscleWiki, WorkoutX, wger and more (I run one of them)</title>
      <dc:creator>Tosh Koevoets</dc:creator>
      <pubDate>Wed, 11 Nov 2026 13:53:14 +0000</pubDate>
      <link>https://dev.to/tosh_koevoets_c167a1efc0b/8-exercise-apis-compared-exercisedb-musclewiki-workoutx-wger-and-more-i-run-one-of-them-2e7m</link>
      <guid>https://dev.to/tosh_koevoets_c167a1efc0b/8-exercise-apis-compared-exercisedb-musclewiki-workoutx-wger-and-more-i-run-one-of-them-2e7m</guid>
      <description>&lt;p&gt;I run &lt;a href="https://ymove.app/exercise-api" rel="noopener noreferrer"&gt;Your Move&lt;/a&gt;, an exercise API. Keep that in mind while reading.&lt;/p&gt;

&lt;p&gt;We run a white-label fitness app business. When we needed exercise media for our clients, none of the videos out there were shot well enough for enterprise use, and GIFs were not accurate enough and the motion was not smooth. So we shot everything ourselves. We shoot with personal trainers, physiotherapists and bodybuilders, people who are experts in their field and in movement.&lt;/p&gt;

&lt;p&gt;We launched the API in February 2026 and have had thousands of signups since, from corporates to small startups to solo founders. We keep shooting and improving the library every month. Along the way I have looked at every other exercise API out there, and this is the list I wish had existed when I started.&lt;/p&gt;

&lt;p&gt;The one question that decides everything: &lt;strong&gt;what media does the API ship?&lt;/strong&gt; Exercise names and muscle data are a commodity now. The GIFs, animations or videos are what your users actually see, and they decide whether your app looks like a product or a prototype.&lt;/p&gt;

&lt;h2&gt;
  
  
  The 8, side by side
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;API&lt;/th&gt;
&lt;th&gt;Media&lt;/th&gt;
&lt;th&gt;Catalog&lt;/th&gt;
&lt;th&gt;Paid from&lt;/th&gt;
&lt;th&gt;Try before paying&lt;/th&gt;
&lt;th&gt;Commercial use&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;a href="https://github.com/ymove-app/exercise-api" rel="noopener noreferrer"&gt;Your Move&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;Real HD video, filmed in one white studio&lt;/td&gt;
&lt;td&gt;2,200+ exercises, 2,600+ videos&lt;/td&gt;
&lt;td&gt;$19/mo&lt;/td&gt;
&lt;td&gt;7-day trial&lt;/td&gt;
&lt;td&gt;All paid plans&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;MuscleWiki API&lt;/td&gt;
&lt;td&gt;Real video, front and side views&lt;/td&gt;
&lt;td&gt;1,900+ exercises, 7,700+ videos&lt;/td&gt;
&lt;td&gt;$99.99/mo for full features ($10 test plan)&lt;/td&gt;
&lt;td&gt;500 calls/mo, playground only&lt;/td&gt;
&lt;td&gt;All paid plans&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;WorkoutX&lt;/td&gt;
&lt;td&gt;3D animation, 720p MP4&lt;/td&gt;
&lt;td&gt;1,900+ exercises, 2,700+ clips&lt;/td&gt;
&lt;td&gt;$19/mo&lt;/td&gt;
&lt;td&gt;500 requests total&lt;/td&gt;
&lt;td&gt;Paid plans&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;ExerciseDB V2 (AscendAPI)&lt;/td&gt;
&lt;td&gt;Animated GIFs, also as MP4&lt;/td&gt;
&lt;td&gt;11,000+ exercises (claimed)&lt;/td&gt;
&lt;td&gt;Via RapidAPI&lt;/td&gt;
&lt;td&gt;2,000 requests/mo, 200 exercises&lt;/td&gt;
&lt;td&gt;Not stated in docs&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;API Ninjas Exercises&lt;/td&gt;
&lt;td&gt;Images only, on premium&lt;/td&gt;
&lt;td&gt;3,000+ exercises&lt;/td&gt;
&lt;td&gt;$39/mo&lt;/td&gt;
&lt;td&gt;3,000 calls/mo, evaluation only&lt;/td&gt;
&lt;td&gt;Paid plans&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;ExerciseDB V1 (open source)&lt;/td&gt;
&lt;td&gt;180p GIFs&lt;/td&gt;
&lt;td&gt;1,500 exercises&lt;/td&gt;
&lt;td&gt;Free, hosted&lt;/td&gt;
&lt;td&gt;No key, 1,000 req/hour&lt;/td&gt;
&lt;td&gt;Not stated&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;wger&lt;/td&gt;
&lt;td&gt;Images, a few user videos&lt;/td&gt;
&lt;td&gt;954 exercises, 78 videos&lt;/td&gt;
&lt;td&gt;Free, open source&lt;/td&gt;
&lt;td&gt;Public API or self-host&lt;/td&gt;
&lt;td&gt;Per-entry Creative Commons&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;buzops9/exercisedb&lt;/td&gt;
&lt;td&gt;GIF-style animation as 1080p MP4&lt;/td&gt;
&lt;td&gt;317 exercises, 593 clips&lt;/td&gt;
&lt;td&gt;Free, self-host&lt;/td&gt;
&lt;td&gt;Open source&lt;/td&gt;
&lt;td&gt;Code MIT, media license not stated&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Counts and prices checked November 2026. Vendors change these, so verify before you build.&lt;/p&gt;

&lt;h2&gt;
  
  
  How I would pick
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;You want real humans on camera and one consistent look.&lt;/strong&gt; Your Move or MuscleWiki. Your Move has more unique exercises, including yoga and pilates. MuscleWiki has more videos per exercise, with two angles each. Your Move films everything in the same white studio with trainers and physiotherapists, so the library looks like one product inside your app, and the $19 plan already includes workout generation and secondary muscles. MuscleWiki puts those on the $99.99 plan.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Animation is fine for your users.&lt;/strong&gt; WorkoutX if you want 3D, ExerciseDB V2 if you want the biggest catalog and do not mind buying through RapidAPI. Check the commercial terms on ExerciseDB first, the docs do not state them.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;You only need text and muscle data.&lt;/strong&gt; API Ninjas, or wger if you can live with Creative Commons attribution per exercise.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Prototype or hobby project.&lt;/strong&gt; ExerciseDB V1 open source. No key, decent data, GIFs are 180p so do not ship them to paying users.&lt;/p&gt;

&lt;h2&gt;
  
  
  What a real-video response looks like
&lt;/h2&gt;

&lt;p&gt;Every exercise API returns roughly the same shape. The difference is the media field. Here is Your Move, filtered to chest exercises with white-background video:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="n"&gt;res&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nb"&gt;fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="s1"&gt;'https://exercise-api.ymove.app/api/v2/exercises?muscleGroup=chest&amp;amp;hasVideoWhite=true&amp;amp;limit=5'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="s1"&gt;'X-API-Key'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="no"&gt;YMOVE_KEY&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;data&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="c1"&gt;// data[0].videoUrl is a portrait 9:16 MP4 of a trainer, not a GIF&lt;/span&gt;
&lt;span class="c1"&gt;// data[0].muscleGroups, instructions, equipment, difficulty, thumbnailUrl&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Keep the key on your server. Every API on this list wants that, and the ones that run through RapidAPI will bill you for a leaked key.&lt;/p&gt;

&lt;p&gt;The endpoint table, quickstarts for REST, the TypeScript SDK and the MCP server, an SVG body map for every exercise and the full exercise list are in the docs repo: &lt;a href="https://github.com/ymove-app/exercise-api" rel="noopener noreferrer"&gt;https://github.com/ymove-app/exercise-api&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Two things nobody tells you until you are live
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Catalog size is a vanity metric.&lt;/strong&gt; Your users see maybe 150 exercises in their first month. Consistency of the 150 matters more than the other 10,000.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Check the media license separately from the code license.&lt;/strong&gt; Two of the open-source options on this list have MIT code and no stated license on the clips.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The longer version with sources per API, a GIF vs filmed video face-off and a selector that picks for you is on our site: &lt;a href="https://ymove.app/exercise-api/top-exercise-apis" rel="noopener noreferrer"&gt;https://ymove.app/exercise-api/top-exercise-apis&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;If you would rather read code than marketing, start at the repo: &lt;a href="https://github.com/ymove-app/exercise-api" rel="noopener noreferrer"&gt;https://github.com/ymove-app/exercise-api&lt;/a&gt;&lt;/p&gt;

</description>
      <category>api</category>
      <category>webdev</category>
      <category>javascript</category>
      <category>fitness</category>
    </item>
    <item>
      <title>8 nutrition APIs compared: Nutritionix, Edamam, Spoonacular, USDA and more (I run one of them)</title>
      <dc:creator>Tosh Koevoets</dc:creator>
      <pubDate>Wed, 11 Nov 2026 13:52:52 +0000</pubDate>
      <link>https://dev.to/tosh_koevoets_c167a1efc0b/8-nutrition-apis-compared-nutritionix-edamam-spoonacular-usda-and-more-i-run-one-of-them-4c8p</link>
      <guid>https://dev.to/tosh_koevoets_c167a1efc0b/8-nutrition-apis-compared-nutritionix-edamam-spoonacular-usda-and-more-i-run-one-of-them-4c8p</guid>
      <description>&lt;p&gt;I run Your Move, which includes a nutrition API. Disclosure done. I spent a year wiring food databases into a trainer app before we built our own, so I have been a paying customer of half this list.&lt;/p&gt;

&lt;p&gt;Our data is public and free: USDA FoodData Central and Open Food Facts. We structured it, removed the wrong entries and update it regularly, but we do not think it is fair to charge a lot for data that was free to begin with. So it is $19 a month with unlimited food searches and barcode scans. A large medical company that makes insulin pumps recently called us because fatsecret was raising its pricing a lot. They told us our data quality was on the same level, and we told them the $19 plan was enough.&lt;/p&gt;

&lt;p&gt;The three questions that matter: &lt;strong&gt;where does the food data come from, can you look up a barcode, and can you sign up without a sales call.&lt;/strong&gt; Everything else is detail.&lt;/p&gt;

&lt;h2&gt;
  
  
  The 8, side by side
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;API&lt;/th&gt;
&lt;th&gt;Data sources&lt;/th&gt;
&lt;th&gt;Pricing&lt;/th&gt;
&lt;th&gt;Self-serve&lt;/th&gt;
&lt;th&gt;Barcode&lt;/th&gt;
&lt;th&gt;AI food logging&lt;/th&gt;
&lt;th&gt;Recipes included&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Your Move&lt;/td&gt;
&lt;td&gt;USDA FoodData Central + Open Food Facts, 3M+ products, 180+ countries&lt;/td&gt;
&lt;td&gt;$19/mo, 7-day trial&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;Yes (text, photo, audio; Pro plan)&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Edamam&lt;/td&gt;
&lt;td&gt;Own database, ~900k foods&lt;/td&gt;
&lt;td&gt;$9 to $58+/mo per product&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;Separate product&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;USDA FoodData Central&lt;/td&gt;
&lt;td&gt;US government reference data&lt;/td&gt;
&lt;td&gt;Free&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;fatsecret&lt;/td&gt;
&lt;td&gt;Own database, broad international coverage&lt;/td&gt;
&lt;td&gt;Contract (Premier tier)&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Nutritionix&lt;/td&gt;
&lt;td&gt;Own database, strong on US restaurant chains&lt;/td&gt;
&lt;td&gt;Custom (enterprise)&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Open Food Facts&lt;/td&gt;
&lt;td&gt;Community database, global barcodes&lt;/td&gt;
&lt;td&gt;Free (ODbL license)&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Spoonacular&lt;/td&gt;
&lt;td&gt;Own database, recipe-first&lt;/td&gt;
&lt;td&gt;From $5/mo, points-based&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;API Ninjas (Nutrition)&lt;/td&gt;
&lt;td&gt;Not disclosed&lt;/td&gt;
&lt;td&gt;Subscription, full nutrients on premium&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Checked November 2026. Pricing pages move, verify before you commit.&lt;/p&gt;

&lt;h2&gt;
  
  
  How I would pick
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;You need to ship a food logger this quarter.&lt;/strong&gt; Your Move or Edamam. Both are self-serve with barcode lookup and text-to-nutrition. Edamam bills per product, so nutrition analysis plus food database plus recipes is three subscriptions. Your Move is one key for all of it, including the exercise API if you ever need it.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;US restaurant menus are the core of your app.&lt;/strong&gt; Nutritionix. Nobody else has that chain menu data. Budget for a sales conversation.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;International packaged foods.&lt;/strong&gt; Your Move or fatsecret. Both have broad branded coverage across countries, and ours matches theirs. The difference is how you buy: fatsecret is contract only, Your Move is self-serve at $19.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Budget is zero and you can build the product layer yourself.&lt;/strong&gt; USDA FoodData Central for US generic foods, Open Food Facts for barcodes. Both are excellent raw data. Neither gives you serving sizes that just work, deduplication, or a search ranking a user would tolerate. That layer is what you pay the others for.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Recipes first, nutrition second.&lt;/strong&gt; Spoonacular. Its food ontology is genuinely good. Its nutrition data is a side dish.&lt;/p&gt;

&lt;h2&gt;
  
  
  What a nutrition response looks like
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="n"&gt;headers&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="s1"&gt;'X-API-Key'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="no"&gt;YMOVE_KEY&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="n"&gt;base&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'https://exercise-api.ymove.app/api/v2'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="c1"&gt;// search, normalized per 100g so values are comparable&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="n"&gt;foods&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nb"&gt;fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sb"&gt;`${base}/foods?query=chicken%20breast&amp;amp;per=100g`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;headers&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;then&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;r&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="c1"&gt;// barcode, with the country so you get the local label&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="n"&gt;product&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nb"&gt;fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sb"&gt;`${base}/foods/barcode/041631000564?country=NL`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;headers&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;then&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;r&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="c1"&gt;// free text to logged meal (Pro plan)&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="n"&gt;meal&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nb"&gt;fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sb"&gt;`${base}/foods/log/text`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="n"&gt;method&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'POST'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'Content-Type'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'application/json'&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;body&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;stringify&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;text&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'grilled chicken with rice and broccoli'&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;then&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;r&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Three things I learned the hard way
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Ask where the calories come from.&lt;/strong&gt; Several "nutrition APIs" are a thin wrapper over USDA with a markup. That is fine, but you should know, because then you also know what they cannot have: branded products and non-US foods.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Barcode coverage is regional.&lt;/strong&gt; A database that is great in the US can be empty in Brazil or India. Test with ten barcodes from your actual market before you sign anything.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Serving sizes break more apps than missing foods do.&lt;/strong&gt; Make sure every food comes with gram weights for its household units, or you will be writing a conversion table yourself at 2am.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The full comparison with sources and a decision table is here: &lt;a href="https://ymove.app/nutrition-api/top-nutrition-apis" rel="noopener noreferrer"&gt;https://ymove.app/nutrition-api/top-nutrition-apis&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Wrong about your API? Comment and I will correct it.&lt;/p&gt;

</description>
      <category>api</category>
      <category>webdev</category>
      <category>javascript</category>
      <category>nutrition</category>
    </item>
    <item>
      <title>How to add real exercise videos to your fitness app in 5 minutes</title>
      <dc:creator>Tosh Koevoets</dc:creator>
      <pubDate>Sun, 11 Oct 2026 15:26:57 +0000</pubDate>
      <link>https://dev.to/tosh_koevoets_c167a1efc0b/how-to-add-real-exercise-videos-to-your-fitness-app-in-5-minutes-40ce</link>
      <guid>https://dev.to/tosh_koevoets_c167a1efc0b/how-to-add-real-exercise-videos-to-your-fitness-app-in-5-minutes-40ce</guid>
      <description>&lt;p&gt;We run an app builder for trainers and gyms. That is our main business. To make it work we filmed our own exercise library, and then something we did not plan happened: people kept asking if they could use our videos in their own apps.&lt;/p&gt;

&lt;p&gt;That is how the &lt;a href="https://ymove.app/exercise-api" rel="noopener noreferrer"&gt;YMove Exercise API&lt;/a&gt; started.&lt;/p&gt;

&lt;p&gt;A lot of exercise data out there comes as animated GIFs. Some people like them, and that is fine, it is partly a matter of taste. But we kept hearing the same complaint: they are not accurate. I agree. For a simple bicep curl a drawing is enough. For a Romanian deadlift, a Turkish get-up or a Bulgarian split squat, you want to see a real person do it: where the hips go, how the back stays straight, what the tempo looks like.&lt;/p&gt;

&lt;p&gt;So we film everything with real people (physiotherapists, personal trainers and former gymnasts) in a white studio, so every clip has the same look next to the others in your app. No AI-generated movement. 1,413+ exercises so far.&lt;/p&gt;

&lt;p&gt;Full disclosure: this is my own API. The patterns below (keeping the key on the server, portrait video, expiring URLs) apply to any exercise video source.&lt;/p&gt;

&lt;p&gt;Here is how to go from zero to a page that plays exercise videos and generates a workout. Want to skip ahead? There are complete examples on GitHub: a &lt;a href="https://github.com/ymove-app/ymove-mobile-fitness-expo" rel="noopener noreferrer"&gt;React Native workout player built with Expo&lt;/a&gt; and a &lt;a href="https://github.com/ymove-app/ymove-exercise-browser" rel="noopener noreferrer"&gt;Next.js exercise browser&lt;/a&gt;. Both run with the free trial key.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F8gq2dfbpxl69cdqe0cbt.gif" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F8gq2dfbpxl69cdqe0cbt.gif" alt="A barbell back squat filmed in our studio, playing as a portrait video in a phone app" width="360" height="611"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  1. Get a key and keep it on the server
&lt;/h2&gt;

&lt;p&gt;Sign up for a &lt;a href="https://ymove.app/exercise-api/signup" rel="noopener noreferrer"&gt;free trial key&lt;/a&gt;. Every request takes it as a header:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;X-API-Key: your_key_here
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Don't put the key in browser code. Add a tiny proxy instead. Here is one in Express:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// server.js&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;express&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;express&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;app&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;express&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;BASE&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;https://exercise-api.ymove.app/api/v2&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="nx"&gt;app&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;/api/exercises&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;params&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;URLSearchParams&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;query&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;r&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;BASE&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;/exercises?&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;params&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;X-API-Key&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;YMOVE_API_KEY&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="p"&gt;});&lt;/span&gt;
  &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;status&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;r&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;status&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;r&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;());&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;

&lt;span class="nx"&gt;app&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;use&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;express&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;static&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;public&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
&lt;span class="nx"&gt;app&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;listen&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;3000&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The same idea works as a Next.js route handler or a Cloudflare Worker.&lt;/p&gt;

&lt;h2&gt;
  
  
  2. Search exercises
&lt;/h2&gt;

&lt;p&gt;The &lt;code&gt;/exercises&lt;/code&gt; endpoint takes filters like &lt;code&gt;muscleGroup&lt;/code&gt;, &lt;code&gt;equipment&lt;/code&gt;, &lt;code&gt;difficulty&lt;/code&gt; and a free-text &lt;code&gt;search&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;/api/exercises?muscleGroup=glutes&amp;amp;equipment=dumbbell&amp;amp;difficulty=beginner&amp;amp;pageSize=6&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;pagination&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Each exercise comes back with what you need to render a card:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"title"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Dumbbell Bench Press"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"slug"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"dumbbell-bench-press"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"instructions"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"Lie on a flat bench..."&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Press the dumbbells up..."&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"importantPoints"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"Keep your feet flat..."&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"muscleGroup"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"chest"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"secondaryMuscles"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"triceps"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"shoulders"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"equipment"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"dumbbell"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"difficulty"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"intermediate"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"videoUrl"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"https://...play_720p.mp4?token=..."&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"videoHlsUrl"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"https://...playlist.m3u8?token=..."&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"thumbnailUrl"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"https://...thumbnail.jpg?token=..."&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"videos"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"tag"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"white-background"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"orientation"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"portrait"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"isPrimary"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}]&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  3. Play the video (two gotchas)
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Gotcha 1: most clips are portrait.&lt;/strong&gt; They are filmed for phones, roughly 9:16. If you drop them in a default 16:9 player you get thick black bars and it looks broken. Read &lt;code&gt;orientation&lt;/code&gt; and size the box to match:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;div&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"clip"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;video&lt;/span&gt; &lt;span class="na"&gt;id=&lt;/span&gt;&lt;span class="s"&gt;"v"&lt;/span&gt; &lt;span class="na"&gt;muted&lt;/span&gt; &lt;span class="na"&gt;loop&lt;/span&gt; &lt;span class="na"&gt;playsinline&lt;/span&gt; &lt;span class="na"&gt;autoplay&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&amp;lt;/video&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/div&amp;gt;&lt;/span&gt;

&lt;span class="nt"&gt;&amp;lt;style&amp;gt;&lt;/span&gt;
  &lt;span class="nc"&gt;.clip&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nl"&gt;width&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;240px&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="py"&gt;aspect-ratio&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;9&lt;/span&gt; &lt;span class="p"&gt;/&lt;/span&gt; &lt;span class="m"&gt;16&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="nc"&gt;.clip&lt;/span&gt; &lt;span class="nt"&gt;video&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nl"&gt;width&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;100%&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nl"&gt;height&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;100%&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nl"&gt;object-fit&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;cover&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nl"&gt;border-radius&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;12px&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/style&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;ex&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;video&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;document&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getElementById&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;v&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="nx"&gt;video&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;poster&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;ex&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;thumbnailUrl&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="nx"&gt;video&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;src&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;ex&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;videoUrl&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="c1"&gt;// MP4 is the simplest; use videoHlsUrl with hls.js for adaptive streaming&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Gotcha 2: video URLs expire after 48 hours.&lt;/strong&gt; They are signed CDN links. Don't store them in your database. Store the exercise &lt;code&gt;id&lt;/code&gt; or &lt;code&gt;slug&lt;/code&gt; and fetch fresh data when you show the video.&lt;/p&gt;

&lt;h2&gt;
  
  
  4. Generate a whole workout
&lt;/h2&gt;

&lt;p&gt;Searching is nice, but most apps really want "give me a 40 minute upper body session". That is one call:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="nx"&gt;app&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;/api/workout&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;params&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;URLSearchParams&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="na"&gt;muscleGroup&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;upper_body&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;difficulty&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;beginner&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;exerciseCount&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;5&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;});&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;r&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;BASE&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;/workouts/generate?&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;params&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;X-API-Key&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;YMOVE_API_KEY&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="p"&gt;});&lt;/span&gt;
  &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;r&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;());&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You get back the exercises in order, each with &lt;code&gt;sets&lt;/code&gt;, &lt;code&gt;reps&lt;/code&gt; and &lt;code&gt;restSeconds&lt;/code&gt;, plus a warmup and a cooldown block and an &lt;code&gt;estimatedMinutes&lt;/code&gt;. Render it as a list of the video cards from step 3 and you have a usable workout screen.&lt;/p&gt;

&lt;p&gt;There is also &lt;code&gt;/programs/generate&lt;/code&gt; for multi-week plans (4, 8 or 12 weeks) if you want to go further.&lt;/p&gt;

&lt;h2&gt;
  
  
  5. Browse without burning your quota
&lt;/h2&gt;

&lt;p&gt;Plans cap how many distinct exercises you can load per month. While you build your UI, pass &lt;code&gt;includeVideos=false&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="nf"&gt;fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;/api/exercises?muscleGroup=core&amp;amp;includeVideos=false&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You get all the text data and no video fields, and it doesn't count toward the cap. Switch it back on when you need the clips.&lt;/p&gt;

&lt;h2&gt;
  
  
  6. Four lessons from running this in production
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Gyms have bad reception.&lt;/strong&gt; Basements, concrete walls, thick steel. Your users will open a workout and stare at a spinning loader. What works for us:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;When the user opens a workout while still online, download the clips for that session in the background.&lt;/li&gt;
&lt;li&gt;Store each file under the exercise &lt;code&gt;id&lt;/code&gt; (or the video &lt;code&gt;id&lt;/code&gt;), never under the signed URL. The URL changes, the id does not.&lt;/li&gt;
&lt;li&gt;During the workout, play the local file first and only stream when there is no local copy.&lt;/li&gt;
&lt;li&gt;Log sets, reps and weights locally too, and sync them to your backend once the phone is back online.&lt;/li&gt;
&lt;li&gt;Next time the app is online, fetch fresh exercise data and replace any clips that changed.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;In a native app you can do this with the platform's file download (for example &lt;code&gt;expo-file-system&lt;/code&gt; in React Native). On the web, a service worker with the Cache API does the same job.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Video speed depends on where your users are.&lt;/strong&gt; If you host the video files yourself, a single server in Europe feels fast in Amsterdam and painfully slow in Bali or Sydney. Put the files behind a CDN with edge locations close to your users. We serve every clip through a worldwide CDN for exactly this reason, and the HLS stream (&lt;code&gt;videoHlsUrl&lt;/code&gt;) adapts the quality to a weak connection instead of stalling.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Preload the next exercise during the rest timer.&lt;/strong&gt; Nothing kills the flow of a workout like a loader between sets. While the user rests, start loading the next clip in the background:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;preloadNext&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;nextExercise&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;v&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;document&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;createElement&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;video&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="nx"&gt;v&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;preload&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;auto&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nx"&gt;v&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;muted&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nx"&gt;v&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;src&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;nextExercise&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;videoUrl&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;When the rest timer ends, the next video starts instantly.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Pick the look that fits your app.&lt;/strong&gt; Some exercises come in two styles: a clean white-background studio clip and a gym-shot clip. Use &lt;code&gt;videoTag&lt;/code&gt; to choose which one you get, so a minimal app gets the studio look and a hardcore lifting app gets the gym:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="nf"&gt;fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;/api/exercises?muscleGroup=back&amp;amp;hasVideoGym=true&amp;amp;videoTag=gym-shot&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Wrapping up
&lt;/h2&gt;

&lt;p&gt;The whole thing is a small proxy, one fetch for search, one for workouts, and a portrait video box. The parts people usually get wrong are the ones that have nothing to do with the API: leaking the key, forcing portrait clips into a landscape player, caching URLs that expire, and forgetting that people train where the signal is bad.&lt;/p&gt;

&lt;p&gt;The full React Native example is at &lt;a href="https://github.com/ymove-app/ymove-mobile-fitness-expo" rel="noopener noreferrer"&gt;ymove-app/ymove-mobile-fitness-expo&lt;/a&gt;. If you try it, I'd like to hear what you build. And if you prefer another source for exercise data, the tips above still apply.&lt;/p&gt;

&lt;p&gt;P.S. Workouts are only half the picture, so we ended up adding a &lt;a href="https://ymove.app/nutrition-api" rel="noopener noreferrer"&gt;nutrition API&lt;/a&gt; as well: a food database, barcode lookup, recipes and meal plans. That one deserves its own post.&lt;/p&gt;

</description>
      <category>javascript</category>
      <category>webdev</category>
      <category>api</category>
      <category>tutorial</category>
    </item>
  </channel>
</rss>
