<?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: Dietly</title>
    <description>The latest articles on DEV Community by Dietly (@dietly).</description>
    <link>https://dev.to/dietly</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%2F4034160%2Fe4db8fe8-19bd-4646-8d00-61180f36dbad.png</url>
      <title>DEV Community: Dietly</title>
      <link>https://dev.to/dietly</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/dietly"/>
    <language>en</language>
    <item>
      <title>DietlyAPI vs FatSecret: nutrition API comparison (2026)</title>
      <dc:creator>Dietly</dc:creator>
      <pubDate>Wed, 22 Jul 2026 19:26:57 +0000</pubDate>
      <link>https://dev.to/dietly/dietlyapi-vs-fatsecret-nutrition-api-comparison-2026-4hem</link>
      <guid>https://dev.to/dietly/dietlyapi-vs-fatsecret-nutrition-api-comparison-2026-4hem</guid>
      <description>&lt;h1&gt;
  
  
  DietlyAPI vs FatSecret
&lt;/h1&gt;

&lt;p&gt;For the core job of high-volume food search and barcode lookup, DietlyAPI is the stronger default: its pricing and limits are published, paid plans have no monthly call cap, and the API is built around that workload. FatSecret remains relevant where its specialist features are the deciding requirement.&lt;/p&gt;

&lt;h2&gt;
  
  
  At a glance
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;/th&gt;
&lt;th&gt;DietlyAPI&lt;/th&gt;
&lt;th&gt;FatSecret&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Monthly price&lt;/td&gt;
&lt;td&gt;Free tier; Pro €27/mo; Scale €92/mo&lt;/td&gt;
&lt;td&gt;Basic free tier; Premier by quote&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Rate limit&lt;/td&gt;
&lt;td&gt;Free ~30 req/min per IP; Pro 500 req/min; Scale 3,000 req/min (per account)&lt;/td&gt;
&lt;td&gt;Per-minute limit not publicly specified (Basic)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Included quota&lt;/td&gt;
&lt;td&gt;No monthly call cap on paid plans (rate-limited only)&lt;/td&gt;
&lt;td&gt;5,000 calls/day (Basic)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Underlying data&lt;/td&gt;
&lt;td&gt;Open Food Facts + USDA FoodData Central&lt;/td&gt;
&lt;td&gt;Proprietary database; 58+ country datasets on Premier&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Best at&lt;/td&gt;
&lt;td&gt;High-throughput fuzzy food search, barcode lookup, stable IDs, per-100g nutrients, MCP server&lt;/td&gt;
&lt;td&gt;Free tier, free Premier for eligible startups/nonprofits/students, many localized country datasets&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Trade-offs&lt;/td&gt;
&lt;td&gt;No recipe analysis, no natural-language meal parsing, no image recognition&lt;/td&gt;
&lt;td&gt;Basic capped at 5,000 calls/day; verification/attribution required; US-focused unless on Premier&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  How capacity is metered
&lt;/h2&gt;

&lt;p&gt;FatSecret's free Basic tier caps at 5,000 calls/day; higher volume moves you to quote-based Premier. DietlyAPI's paid plans lift throughput to a fixed per-minute rate at a published price.&lt;/p&gt;

&lt;h2&gt;
  
  
  Which should you choose?
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Specialist case for FatSecret
&lt;/h3&gt;

&lt;p&gt;FatSecret can be a better fit if you qualify for its free Premier programme, or specifically need verified country-specific datasets across many markets.&lt;/p&gt;

&lt;h3&gt;
  
  
  Why DietlyAPI is the stronger default
&lt;/h3&gt;

&lt;p&gt;DietlyAPI is the stronger pick when you need more than 5,000 calls/day without a verification process, at a predictable flat price and a single global catalog.&lt;/p&gt;

&lt;p&gt;For food search and barcode lookup with predictable pricing, DietlyAPI is our recommended choice. Choose FatSecret when the specialist features above matter more than throughput, transparent self-serve pricing and a simple request-based limit.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Try DietlyAPI free.&lt;/strong&gt; Public read endpoints work with no key at a shared per-IP limit — enough to prototype in minutes. &lt;br&gt;
&lt;a href="https://www.getdietly.com/api" rel="noopener noreferrer"&gt;Get the API&lt;/a&gt;&lt;/p&gt;

</description>
      <category>api</category>
      <category>webdev</category>
      <category>data</category>
      <category>nutrition</category>
    </item>
    <item>
      <title>DietlyAPI vs Nutritionix: nutrition API comparison (2026)</title>
      <dc:creator>Dietly</dc:creator>
      <pubDate>Wed, 22 Jul 2026 19:26:56 +0000</pubDate>
      <link>https://dev.to/dietly/dietlyapi-vs-nutritionix-nutrition-api-comparison-2026-5hca</link>
      <guid>https://dev.to/dietly/dietlyapi-vs-nutritionix-nutrition-api-comparison-2026-5hca</guid>
      <description>&lt;h1&gt;
  
  
  DietlyAPI vs Nutritionix
&lt;/h1&gt;

&lt;p&gt;For the core job of high-volume food search and barcode lookup, DietlyAPI is the stronger default: its pricing and limits are published, paid plans have no monthly call cap, and the API is built around that workload. Nutritionix remains relevant where its specialist features are the deciding requirement.&lt;/p&gt;

&lt;h2&gt;
  
  
  At a glance
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;/th&gt;
&lt;th&gt;DietlyAPI&lt;/th&gt;
&lt;th&gt;Nutritionix&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Monthly price&lt;/td&gt;
&lt;td&gt;Free tier; Pro €27/mo; Scale €92/mo&lt;/td&gt;
&lt;td&gt;Quote / contact sales (Syndigo)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Rate limit&lt;/td&gt;
&lt;td&gt;Free ~30 req/min per IP; Pro 500 req/min; Scale 3,000 req/min (per account)&lt;/td&gt;
&lt;td&gt;Not publicly specified&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Included quota&lt;/td&gt;
&lt;td&gt;No monthly call cap on paid plans (rate-limited only)&lt;/td&gt;
&lt;td&gt;Not publicly specified&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Underlying data&lt;/td&gt;
&lt;td&gt;Open Food Facts + USDA FoodData Central&lt;/td&gt;
&lt;td&gt;600K+ food database (US-focused), branded + restaurant items&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Best at&lt;/td&gt;
&lt;td&gt;High-throughput fuzzy food search, barcode lookup, stable IDs, per-100g nutrients, MCP server&lt;/td&gt;
&lt;td&gt;Natural-language food logging, instant search, exercise parsing&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Trade-offs&lt;/td&gt;
&lt;td&gt;No recipe analysis, no natural-language meal parsing, no image recognition&lt;/td&gt;
&lt;td&gt;Pricing and limits not published; enterprise/quote sales motion&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  How capacity is metered
&lt;/h2&gt;

&lt;p&gt;Nutritionix (owned by Syndigo) publishes neither numeric rate limits nor list pricing, so capacity and cost are established by quote. DietlyAPI publishes its plans, limits and OpenAPI spec openly.&lt;/p&gt;

&lt;h2&gt;
  
  
  Which should you choose?
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Specialist case for Nutritionix
&lt;/h3&gt;

&lt;p&gt;Nutritionix is a better fit specifically when natural-language food logging (“two eggs and toast” → nutrients) and a large US restaurant/branded catalog are central to your product.&lt;/p&gt;

&lt;h3&gt;
  
  
  Why DietlyAPI is the stronger default
&lt;/h3&gt;

&lt;p&gt;DietlyAPI is the stronger pick when you want transparent, self-serve pricing and high-throughput search/barcode access without contacting sales for a quote.&lt;/p&gt;

&lt;p&gt;For food search and barcode lookup with predictable pricing, DietlyAPI is our recommended choice. Choose Nutritionix when the specialist features above matter more than throughput, transparent self-serve pricing and a simple request-based limit.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Try DietlyAPI free.&lt;/strong&gt; Public read endpoints work with no key at a shared per-IP limit — enough to prototype in minutes. &lt;br&gt;
&lt;a href="https://www.getdietly.com/api" rel="noopener noreferrer"&gt;Get the API&lt;/a&gt;&lt;/p&gt;

</description>
      <category>api</category>
      <category>webdev</category>
      <category>data</category>
      <category>programming</category>
    </item>
    <item>
      <title>DietlyAPI vs Spoonacular: nutrition API comparison (2026)</title>
      <dc:creator>Dietly</dc:creator>
      <pubDate>Wed, 22 Jul 2026 19:26:12 +0000</pubDate>
      <link>https://dev.to/dietly/dietlyapi-vs-spoonacular-nutrition-api-comparison-2026-3fk1</link>
      <guid>https://dev.to/dietly/dietlyapi-vs-spoonacular-nutrition-api-comparison-2026-3fk1</guid>
      <description>&lt;h1&gt;
  
  
  DietlyAPI vs Spoonacular
&lt;/h1&gt;

&lt;p&gt;For the core job of high-volume food search and barcode lookup, DietlyAPI is the stronger default: its pricing and limits are published, paid plans have no monthly call cap, and the API is built around that workload. Spoonacular remains relevant where its specialist features are the deciding requirement.&lt;/p&gt;

&lt;h2&gt;
  
  
  At a glance
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;/th&gt;
&lt;th&gt;DietlyAPI&lt;/th&gt;
&lt;th&gt;Spoonacular&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Monthly price&lt;/td&gt;
&lt;td&gt;Free tier; Pro €27/mo; Scale €92/mo&lt;/td&gt;
&lt;td&gt;Cook US$29/mo (and other tiers)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Rate limit&lt;/td&gt;
&lt;td&gt;Free ~30 req/min per IP; Pro 500 req/min; Scale 3,000 req/min (per account)&lt;/td&gt;
&lt;td&gt;5 requests/sec&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Included quota&lt;/td&gt;
&lt;td&gt;No monthly call cap on paid plans (rate-limited only)&lt;/td&gt;
&lt;td&gt;1,500 points/day (point-weighted: some calls cost &amp;gt;1 point)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Underlying data&lt;/td&gt;
&lt;td&gt;Open Food Facts + USDA FoodData Central&lt;/td&gt;
&lt;td&gt;Recipe, ingredient and product database&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Best at&lt;/td&gt;
&lt;td&gt;High-throughput fuzzy food search, barcode lookup, stable IDs, per-100g nutrients, MCP server&lt;/td&gt;
&lt;td&gt;Recipe search, meal planning, ingredient substitution, wine pairing&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Trade-offs&lt;/td&gt;
&lt;td&gt;No recipe analysis, no natural-language meal parsing, no image recognition&lt;/td&gt;
&lt;td&gt;Point-weighted quota; cached data generally must be deleted after 1 hour&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  How capacity is metered
&lt;/h2&gt;

&lt;p&gt;Spoonacular bills in points, and some endpoints or larger result sets cost more than one point, so a nominal daily quota does not map one-to-one to calls. DietlyAPI counts plain requests against a per-minute rate with no point weighting.&lt;/p&gt;

&lt;h2&gt;
  
  
  Which should you choose?
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Specialist case for Spoonacular
&lt;/h3&gt;

&lt;p&gt;Spoonacular is a better fit when recipes are central: recipe discovery, meal planning and ingredient workflows are its core, and DietlyAPI does not offer them.&lt;/p&gt;

&lt;h3&gt;
  
  
  Why DietlyAPI is the stronger default
&lt;/h3&gt;

&lt;p&gt;DietlyAPI is the stronger pick for a food/nutrition catalog: fuzzy product search and barcode lookup at high volume, with simple per-request behaviour instead of a point budget.&lt;/p&gt;

&lt;p&gt;For food search and barcode lookup with predictable pricing, DietlyAPI is our recommended choice. Choose Spoonacular when the specialist features above matter more than throughput, transparent self-serve pricing and a simple request-based limit.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Try DietlyAPI free.&lt;/strong&gt; Public read endpoints work with no key at a shared per-IP limit — enough to prototype in minutes. &lt;br&gt;
&lt;a href="https://www.getdietly.com/api" rel="noopener noreferrer"&gt;Get the API&lt;/a&gt;&lt;/p&gt;

</description>
      <category>api</category>
      <category>webdev</category>
      <category>programming</category>
      <category>food</category>
    </item>
    <item>
      <title>DietlyAPI vs Edamam: nutrition API comparison (2026)</title>
      <dc:creator>Dietly</dc:creator>
      <pubDate>Wed, 22 Jul 2026 19:26:11 +0000</pubDate>
      <link>https://dev.to/dietly/dietlyapi-vs-edamam-nutrition-api-comparison-2026-9d</link>
      <guid>https://dev.to/dietly/dietlyapi-vs-edamam-nutrition-api-comparison-2026-9d</guid>
      <description>&lt;h1&gt;
  
  
  DietlyAPI vs Edamam
&lt;/h1&gt;

&lt;p&gt;For the core job of high-volume food search and barcode lookup, DietlyAPI is the stronger default: its pricing and limits are published, paid plans have no monthly call cap, and the API is built around that workload. Edamam remains relevant where its specialist features are the deciding requirement.&lt;/p&gt;

&lt;h2&gt;
  
  
  At a glance
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;/th&gt;
&lt;th&gt;DietlyAPI&lt;/th&gt;
&lt;th&gt;Edamam&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Monthly price&lt;/td&gt;
&lt;td&gt;Free tier; Pro €27/mo; Scale €92/mo&lt;/td&gt;
&lt;td&gt;Basic US$14/mo (Core US$69, Plus US$299)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Rate limit&lt;/td&gt;
&lt;td&gt;Free ~30 req/min per IP; Pro 500 req/min; Scale 3,000 req/min (per account)&lt;/td&gt;
&lt;td&gt;50 food requests/min (Basic)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Included quota&lt;/td&gt;
&lt;td&gt;No monthly call cap on paid plans (rate-limited only)&lt;/td&gt;
&lt;td&gt;100,000 calls/month (Basic)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Underlying data&lt;/td&gt;
&lt;td&gt;Open Food Facts + USDA FoodData Central&lt;/td&gt;
&lt;td&gt;Proprietary food database + branded/grocery data&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Best at&lt;/td&gt;
&lt;td&gt;High-throughput fuzzy food search, barcode lookup, stable IDs, per-100g nutrients, MCP server&lt;/td&gt;
&lt;td&gt;Natural-language ingredient parser, measures, diet/allergy filters&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Trade-offs&lt;/td&gt;
&lt;td&gt;No recipe analysis, no natural-language meal parsing, no image recognition&lt;/td&gt;
&lt;td&gt;Monthly call quota caps sustained throughput; caching restricted&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  How capacity is metered
&lt;/h2&gt;

&lt;p&gt;Edamam meters a fixed monthly quota (100,000 calls on Basic), so sustained throughput is bounded by that allowance. DietlyAPI's paid plans apply only a per-minute rate ceiling with no monthly cap, so capacity scales with the rate rather than a credit balance.&lt;/p&gt;

&lt;h2&gt;
  
  
  Which should you choose?
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Specialist case for Edamam
&lt;/h3&gt;

&lt;p&gt;Edamam is a better fit specifically when natural-language recipe/ingredient parsing, measure conversion, or built-in diet and allergy filtering out of the box.&lt;/p&gt;

&lt;h3&gt;
  
  
  Why DietlyAPI is the stronger default
&lt;/h3&gt;

&lt;p&gt;DietlyAPI is the stronger pick when you need high request volume for plain food search and barcode lookup at a flat monthly price, without a monthly call allowance to manage.&lt;/p&gt;

&lt;p&gt;For food search and barcode lookup with predictable pricing, DietlyAPI is our recommended choice. Choose Edamam when the specialist features above matter more than throughput, transparent self-serve pricing and a simple request-based limit.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Try DietlyAPI free.&lt;/strong&gt; Public read endpoints work with no key at a shared per-IP limit — enough to prototype in minutes. &lt;br&gt;
&lt;a href="https://www.getdietly.com/api" rel="noopener noreferrer"&gt;Get the API&lt;/a&gt;&lt;/p&gt;

</description>
      <category>api</category>
      <category>webdev</category>
      <category>programming</category>
      <category>nutrition</category>
    </item>
    <item>
      <title>Nutrition API comparison (2026): 7 providers by price, limits &amp; features</title>
      <dc:creator>Dietly</dc:creator>
      <pubDate>Wed, 22 Jul 2026 19:25:26 +0000</pubDate>
      <link>https://dev.to/dietly/nutrition-api-comparison-2026-7-providers-by-price-limits-features-25cg</link>
      <guid>https://dev.to/dietly/nutrition-api-comparison-2026-7-providers-by-price-limits-features-25cg</guid>
      <description>&lt;h1&gt;
  
  
  Nutrition API comparison (2026)
&lt;/h1&gt;

&lt;p&gt;For high-volume food search and barcode lookup, DietlyAPI is the strongest fit in this comparison: paid plans combine published throughput with no monthly call cap. Other providers can still be the right choice when recipes, natural-language parsing, downloadable open data or authoritative US reference values are the main requirement.&lt;/p&gt;

&lt;p&gt;We build DietlyAPI and believe its focused, self-serve model is the better default for most food-search and barcode products. The figures below remain verifiable, and we still identify the narrower workflows where another provider has a genuine advantage.&lt;/p&gt;

&lt;h2&gt;
  
  
  The comparison table
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;API / plan&lt;/th&gt;
&lt;th&gt;Type&lt;/th&gt;
&lt;th&gt;Monthly price&lt;/th&gt;
&lt;th&gt;Rate limit&lt;/th&gt;
&lt;th&gt;Included quota&lt;/th&gt;
&lt;th&gt;Best at&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Open Food Facts&lt;/td&gt;
&lt;td&gt;Open, crowdsourced&lt;/td&gt;
&lt;td&gt;Free&lt;/td&gt;
&lt;td&gt;10 searches/min; 15 product reads/min per IP&lt;/td&gt;
&lt;td&gt;No published monthly cap&lt;/td&gt;
&lt;td&gt;Open worldwide packaged-product data; bulk export / self-hosting&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;USDA FoodData Central&lt;/td&gt;
&lt;td&gt;Public reference&lt;/td&gt;
&lt;td&gt;Free&lt;/td&gt;
&lt;td&gt;1,000 requests/hour per IP&lt;/td&gt;
&lt;td&gt;No published monthly cap&lt;/td&gt;
&lt;td&gt;Authoritative US reference data (free key required)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;FatSecret (Basic)&lt;/td&gt;
&lt;td&gt;Commercial&lt;/td&gt;
&lt;td&gt;Free tier&lt;/td&gt;
&lt;td&gt;Not publicly specified&lt;/td&gt;
&lt;td&gt;5,000 calls/day&lt;/td&gt;
&lt;td&gt;Free tier; localized country datasets on Premier&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Edamam (Basic)&lt;/td&gt;
&lt;td&gt;Commercial&lt;/td&gt;
&lt;td&gt;US$14/mo&lt;/td&gt;
&lt;td&gt;50 food requests/min&lt;/td&gt;
&lt;td&gt;100,000 calls/month&lt;/td&gt;
&lt;td&gt;NLP ingredient parser, measures, diet/allergy filters&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Spoonacular (Cook)&lt;/td&gt;
&lt;td&gt;Commercial&lt;/td&gt;
&lt;td&gt;US$29/mo&lt;/td&gt;
&lt;td&gt;5 requests/sec&lt;/td&gt;
&lt;td&gt;1,500 points/day (weighted)&lt;/td&gt;
&lt;td&gt;Recipes, meal planning, ingredient workflows&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Nutritionix&lt;/td&gt;
&lt;td&gt;Commercial&lt;/td&gt;
&lt;td&gt;Quote / contact&lt;/td&gt;
&lt;td&gt;Not specified&lt;/td&gt;
&lt;td&gt;Not specified&lt;/td&gt;
&lt;td&gt;Natural-language food logging; large US catalog&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;DietlyAPI (Pro)&lt;/td&gt;
&lt;td&gt;Commercial&lt;/td&gt;
&lt;td&gt;€27/mo&lt;/td&gt;
&lt;td&gt;500 requests/min per account&lt;/td&gt;
&lt;td&gt;No monthly call cap&lt;/td&gt;
&lt;td&gt;High-throughput fuzzy search + barcode lookup&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;DietlyAPI (Scale)&lt;/td&gt;
&lt;td&gt;Commercial&lt;/td&gt;
&lt;td&gt;€92/mo&lt;/td&gt;
&lt;td&gt;3,000 requests/min per account&lt;/td&gt;
&lt;td&gt;No monthly call cap&lt;/td&gt;
&lt;td&gt;As Pro, higher throughput&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Providers meter capacity differently. Some (Edamam, Spoonacular, FatSecret) enforce a monthly or daily quota, so sustained throughput is bounded by that allowance. Others (USDA, DietlyAPI) apply a per-interval rate ceiling with no separate monthly credit, so capacity depends on the sustained rate. Spoonacular’s point weighting means a nominal quota does not map one-to-one to calls.&lt;/p&gt;

&lt;h2&gt;
  
  
  What each option is best at
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Open Food Facts
&lt;/h3&gt;

&lt;p&gt;A free, crowdsourced global database of packaged products, keyed by barcode. Reads need no login, but the hosted search endpoint is limited (10 req/min/IP) and is not intended for search-as-you-type; high-volume users are pointed to bulk exports or self-hosting.&lt;/p&gt;

&lt;h3&gt;
  
  
  USDA FoodData Central
&lt;/h3&gt;

&lt;p&gt;The authoritative US reference dataset (Foundation, FNDDS, SR Legacy and branded foods), free and in the public domain. Its API needs a free data.gov key and defaults to 1,000 requests/hour per IP.&lt;/p&gt;

&lt;h3&gt;
  
  
  FatSecret
&lt;/h3&gt;

&lt;p&gt;A commercial platform whose free Basic tier is capped at 5,000 calls/day. Premier adds many country datasets, but pricing is quote-based and its free programme is limited to eligible applicants after verification.&lt;/p&gt;

&lt;h3&gt;
  
  
  Edamam
&lt;/h3&gt;

&lt;p&gt;Its Basic plan adds an ingredient parser, measures and diet/allergy filters for US$14/month, but the 100,000-call monthly quota bounds sustained use and caching rights are restricted.&lt;/p&gt;

&lt;h3&gt;
  
  
  Spoonacular
&lt;/h3&gt;

&lt;p&gt;Recipe-oriented. The US$29 Cook plan covers recipe and ingredient tooling with a point-weighted daily quota; it is not a like-for-like food catalog, and cached data generally must be deleted after an hour.&lt;/p&gt;

&lt;h3&gt;
  
  
  Nutritionix
&lt;/h3&gt;

&lt;p&gt;Owned by Syndigo, focused on natural-language food logging over a large US-centric catalog. Current numeric pricing and limits are not published; access is quote-based.&lt;/p&gt;

&lt;h3&gt;
  
  
  DietlyAPI
&lt;/h3&gt;

&lt;p&gt;The strongest fit here for high-throughput fuzzy food search and barcode lookup: transparent self-serve pricing, stable IDs, per-100g nutrients and paid plans with no monthly call cap. It deliberately focuses on catalog lookup rather than recipe analysis, NLP meal parsing or image recognition.&lt;/p&gt;

&lt;h2&gt;
  
  
  How to choose
&lt;/h2&gt;

&lt;p&gt;Start with DietlyAPI when your product needs dependable food search, barcode lookup and predictable capacity. Move to a specialist alternative when one of the narrower workflows below is central to the product.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;High-throughput fuzzy search + barcode lookup, flat pricing&lt;/strong&gt; — DietlyAPI.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Open, downloadable worldwide product data&lt;/strong&gt; — Open Food Facts.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Authoritative US reference values&lt;/strong&gt; — USDA FoodData Central.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Natural-language parsing, measures, diet/allergy filters&lt;/strong&gt; — Edamam.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Recipes and ingredient workflows&lt;/strong&gt; — Spoonacular.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Natural-language food logging (US-centric)&lt;/strong&gt; — Nutritionix.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Head-to-head pages
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Try DietlyAPI free.&lt;/strong&gt; Public read endpoints work with no key at a shared per-IP limit. &lt;br&gt;
&lt;a href="https://www.getdietly.com/api" rel="noopener noreferrer"&gt;Get the API&lt;/a&gt;&lt;/p&gt;

</description>
      <category>api</category>
      <category>webdev</category>
      <category>programming</category>
      <category>database</category>
    </item>
    <item>
      <title>Dietly Food Data Quality Report 2026</title>
      <dc:creator>Dietly</dc:creator>
      <pubDate>Wed, 22 Jul 2026 19:25:25 +0000</pubDate>
      <link>https://dev.to/dietly/dietly-food-data-quality-report-2026-2om0</link>
      <guid>https://dev.to/dietly/dietly-food-data-quality-report-2026-2om0</guid>
      <description>&lt;p&gt;Dietly catalog report · July 2026&lt;/p&gt;

&lt;h1&gt;
  
  
  Inside 4.75 million food records.
&lt;/h1&gt;

&lt;p&gt;&lt;strong&gt;A large catalog is not automatically a usable catalog.&lt;/strong&gt; Dietly measured every record in its production database to see how often essential nutrition fields, identifiers, images and serving information are actually present.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;By the Dietly engineering team · Published July 21, 2026 · Production snapshot: 4,752,013 records&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;55.3%&lt;/strong&gt; contain calories, protein, carbohydrates and fat&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;98.3%&lt;/strong&gt; have a well-formed 8–14 digit barcode&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;65.8%&lt;/strong&gt; include a product image&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;40.3%&lt;/strong&gt; meet Dietly's minimum public-page data rule&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The short version:&lt;/strong&gt; Dietly's advantage is not merely having 4.75 million records; it is turning an uneven source catalog into more useful results. Dietly ranks complete and plausible records first, preserves sparse records for long-tail coverage, and applies a higher threshold before publishing a record as a public nutrition page.&lt;/p&gt;

&lt;h2&gt;
  
  
  Raw catalog quality is not user-facing result quality
&lt;/h2&gt;

&lt;p&gt;This report measures all 4.75 million raw records stored in Dietly's catalog, including incomplete source records retained for long-tail and specific-product coverage. &lt;strong&gt;Dietly does not present every record equally.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Search automatically prioritizes the best available candidates using nutritional completeness, plausibility checks, images, serving information, source confidence and relevance. Records with the same normalized name are consolidated so the stronger candidate is normally shown, while curated food lists exclude incomplete or implausible records entirely.&lt;/p&gt;

&lt;p&gt;Consequently, the percentages below describe the underlying source catalog, not the typical quality of results users receive. Sparse records are generally available as fallbacks when Dietly has no stronger match for a specific product. When an upstream source improves an existing record, Dietly's refresh process can replace the older version of that same source record.&lt;/p&gt;

&lt;h2&gt;
  
  
  What 4.75 million records actually contain
&lt;/h2&gt;

&lt;p&gt;The chart below uses the entire production catalog as its denominator. A field counts as present when the normalized Dietly record contains a non-empty value; presence does not prove that the source label is correct.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;How to read the 55.3% figure:&lt;/strong&gt; it should not be interpreted as meaning 44.7% of normal search results are incomplete. Lower-quality catalog records are deprioritized or excluded during result selection.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Field or rule&lt;/th&gt;
&lt;th&gt;Records&lt;/th&gt;
&lt;th&gt;Coverage&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Any barcode&lt;/td&gt;
&lt;td&gt;4,738,308&lt;/td&gt;
&lt;td&gt;99.7%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Well-formed barcode&lt;/td&gt;
&lt;td&gt;4,669,075&lt;/td&gt;
&lt;td&gt;98.3%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Country metadata&lt;/td&gt;
&lt;td&gt;4,644,862&lt;/td&gt;
&lt;td&gt;97.7%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Brand&lt;/td&gt;
&lt;td&gt;3,292,195&lt;/td&gt;
&lt;td&gt;69.3%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Product image&lt;/td&gt;
&lt;td&gt;3,124,672&lt;/td&gt;
&lt;td&gt;65.8%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;All four core nutrition fields&lt;/td&gt;
&lt;td&gt;2,629,070&lt;/td&gt;
&lt;td&gt;55.3%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Sugar&lt;/td&gt;
&lt;td&gt;2,546,586&lt;/td&gt;
&lt;td&gt;53.6%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Saturated fat&lt;/td&gt;
&lt;td&gt;2,496,562&lt;/td&gt;
&lt;td&gt;52.5%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Sodium&lt;/td&gt;
&lt;td&gt;2,376,654&lt;/td&gt;
&lt;td&gt;50.0%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Category&lt;/td&gt;
&lt;td&gt;2,237,134&lt;/td&gt;
&lt;td&gt;47.1%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Serving size&lt;/td&gt;
&lt;td&gt;1,717,378&lt;/td&gt;
&lt;td&gt;36.1%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Fiber&lt;/td&gt;
&lt;td&gt;1,391,584&lt;/td&gt;
&lt;td&gt;29.3%&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  The useful catalog is a funnel
&lt;/h2&gt;

&lt;p&gt;Dietly does not treat a name and barcode as sufficient for every use case. Search can retain a broad source-labeled inventory, while public nutrition pages require a smaller minimum set of fields.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;4.75M:&lt;/strong&gt; total source-labeled records in the measured catalog&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2.63M:&lt;/strong&gt; records with all four core nutrition fields&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;1.91M:&lt;/strong&gt; records with calories, protein and an image—Dietly's minimum public-page rule&lt;/p&gt;

&lt;p&gt;The public-page rule is deliberately easy to explain and reproduce. It is not a claim that all 1.91 million records should be published or indexed. Page selection can still account for nutrient completeness, source confidence, category diversity and whether a record already has a stable public URL.&lt;/p&gt;

&lt;h2&gt;
  
  
  Completeness and plausibility are different tests
&lt;/h2&gt;

&lt;p&gt;Of the 2,629,070 records with complete core nutrition, 2,622,854—or 99.8%—fall inside Dietly's broad per-100-gram plausibility bounds: 0–900 kcal and 0–100 grams each for protein, fat and carbohydrates. These are guardrails for obvious impossibilities, not laboratory validation.&lt;/p&gt;

&lt;p&gt;A second consistency check compares reported calories with the simple 4/4/9 energy estimate from protein, carbohydrates and fat. Using a tolerance of the larger of 50 kcal or 20% of reported calories, 2,525,253 complete records—96.1%—are internally consistent. Fiber, alcohols, rounding and local label rules can create legitimate differences, so this check is a warning signal rather than a verdict.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Important limitation:&lt;/strong&gt; this report measures coverage and internal consistency. Dietly did not independently laboratory-test the products, and a populated field can still reproduce an incorrect or outdated source label.&lt;/p&gt;

&lt;h2&gt;
  
  
  How Dietly turns uneven source data into useful results
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Keep provenance visible
&lt;/h3&gt;

&lt;p&gt;API results carry a source and confidence value. Dietly's catalog currently combines labeled records from &lt;a href="https://world.openfoodfacts.org/" rel="noopener noreferrer"&gt;Open Food Facts&lt;/a&gt;, &lt;a href="https://fdc.nal.usda.gov/" rel="noopener noreferrer"&gt;USDA FoodData Central&lt;/a&gt; and a very small number of Dietly community or assisted entries. Source coverage and field definitions differ, so consumers should not assume every row was produced through the same process.&lt;/p&gt;

&lt;h3&gt;
  
  
  Rank useful records before sparse ones
&lt;/h3&gt;

&lt;p&gt;Search ranking favors exact names, useful identifiers, complete and plausible core nutrition, images, category information and stronger confidence. Missing nutrients remain missing in the API instead of silently becoming measured zeroes.&lt;/p&gt;

&lt;h3&gt;
  
  
  Separate discovery from publication
&lt;/h3&gt;

&lt;p&gt;A record can help a barcode or text search without deserving a standalone public page. That separation lets Dietly preserve breadth while applying a higher publication threshold to pages intended for people and search engines.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The practical result:&lt;/strong&gt; Dietly keeps the breadth of a multi-million-record catalog without pretending every source row has equal value. Search quality comes from the normalization, confidence signals and selection rules layered on top of that raw coverage.&lt;/p&gt;

&lt;h2&gt;
  
  
  Methodology
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Snapshot
&lt;/h3&gt;

&lt;p&gt;A read-only aggregate query was run against Dietly's production catalog on July 21, 2026. The database contained 4,752,013 records at query time. Percentages use that total unless a different denominator is stated.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Core nutrition:&lt;/strong&gt; Calories, protein, carbohydrates and fat are all present.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Well-formed barcode:&lt;/strong&gt; Contains only digits and is 8–14 characters long. This is a structural check, not a GTIN checksum or ownership check.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Public-page rule:&lt;/strong&gt; Calories, protein and a non-empty image URL are present. This is the generator's minimum data-selection rule, not an indexability guarantee.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Broad plausibility:&lt;/strong&gt; Complete core nutrition with 0–900 kcal and 0–100 g each of protein, fat and carbohydrates per 100 g.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Energy consistency:&lt;/strong&gt; The reported calories are within the larger of 50 kcal or 20% of the 4/4/9 estimate. The test intentionally allows for rounding and nutrients outside that simple model.&lt;/p&gt;

&lt;h3&gt;
  
  
  Privacy and reproducibility
&lt;/h3&gt;

&lt;p&gt;The analysis uses counts and percentages only. No record-level database export accompanies this report. Definitions and exact counts are published so the claims can be evaluated without disclosing Dietly's normalized catalog or internal processing pipeline.&lt;/p&gt;

&lt;h2&gt;
  
  
  Sources and reuse
&lt;/h2&gt;

&lt;p&gt;Open Food Facts database data is reused under the Open Database License; its individual contents use the Database Contents License, and product images have separate licensing terms. USDA FoodData Central data is published in the public domain under CC0. Dietly's measurements, normalization and presentation do not imply that either source endorses Dietly.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;a href="https://openfoodfacts.github.io/documentation/docs/Product-Opener/api/tutorials/license-be-on-the-legal-side/" rel="noopener noreferrer"&gt;Open Food Facts licensing documentation&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;a href="https://fdc.nal.usda.gov/data-documentation/" rel="noopener noreferrer"&gt;USDA FoodData Central data documentation&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;a href="https://www.getdietly.com/developers/missing-nutrition-data/" rel="noopener noreferrer"&gt;Developer guide: handling missing nutrition data&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;a href="https://www.getdietly.com/api" rel="noopener noreferrer"&gt;Explore DietlyAPI&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>datascience</category>
      <category>database</category>
      <category>api</category>
      <category>nutrition</category>
    </item>
    <item>
      <title>API Key Hygiene: Scopes, Expiry, Rotation and 2FA</title>
      <dc:creator>Dietly</dc:creator>
      <pubDate>Wed, 22 Jul 2026 19:02:35 +0000</pubDate>
      <link>https://dev.to/dietly/api-key-hygiene-scopes-expiry-rotation-and-2fa-1gn</link>
      <guid>https://dev.to/dietly/api-key-hygiene-scopes-expiry-rotation-and-2fa-1gn</guid>
      <description>&lt;h1&gt;
  
  
  API key hygiene: rotation, scopes, expiry and 2FA
&lt;/h1&gt;

&lt;p&gt;&lt;strong&gt;The single highest-value change you can make is to stop using one key for&lt;br&gt;
everything.&lt;/strong&gt; One key per service, each with the narrowest scope that still works, means a leak&lt;br&gt;
is a contained incident rather than a full compromise: you rotate the one key, the one service has a&lt;br&gt;
short blip, and nothing else on your account notices. Everything else on this page is a refinement of&lt;br&gt;
that idea. DietlyAPI supports named access keys with independent scopes, expiry dates and per-key rate&lt;br&gt;
caps, so the good structure is available without any extra tooling.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The checklist:&lt;/strong&gt; one key per service, read scope unless you need&lt;br&gt;
writes, an expiry date on anything temporary, a per-key rate cap so one service cannot starve the&lt;br&gt;
others, 2FA on the account itself, and a rotation you have practised before you need it.&lt;/p&gt;
&lt;h2&gt;
  
  
  One key per service
&lt;/h2&gt;

&lt;p&gt;Name keys after where they run: &lt;code&gt;web-prod&lt;/code&gt;, &lt;code&gt;worker-prod&lt;/code&gt;,&lt;br&gt;
&lt;code&gt;ci&lt;/code&gt;, &lt;code&gt;laptop-dev&lt;/code&gt;. This costs nothing and pays back the first time something goes&lt;br&gt;
wrong, because it turns two impossible questions into easy ones. Which service is generating that&lt;br&gt;
traffic spike? The one whose key shows the usage. What do I have to redeploy after this key leaked? The&lt;br&gt;
one service that used it. A single shared key makes both questions unanswerable, and it means every&lt;br&gt;
rotation is a coordinated outage across your whole stack.&lt;/p&gt;

&lt;p&gt;The same logic applies to environments. A development key on a laptop, in a shell history and in&lt;br&gt;
half a dozen scratch scripts is the likeliest key on your account to leak, and it should never be the&lt;br&gt;
same key that serves production traffic.&lt;/p&gt;
&lt;h2&gt;
  
  
  Use read scope unless you need more
&lt;/h2&gt;

&lt;p&gt;Keys carry a scope. A read-scope key can perform GET requests and nothing else; if it is used for&lt;br&gt;
any other method the API refuses with a clear 403:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;{"detail":"This API key is read-only. Use a full-access key for this endpoint."}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Most integrations only ever look data up, so read scope is the correct default rather than a&lt;br&gt;
hardened configuration. It also converts an entire class of incident into a non-event: a leaked&lt;br&gt;
read-only key exposes public catalog data that anyone can already query anonymously. Reach for a&lt;br&gt;
full-access key only for the specific service that genuinely needs to write, and keep that key's blast&lt;br&gt;
radius as small as you can.&lt;/p&gt;
&lt;h2&gt;
  
  
  Put an expiry on anything temporary
&lt;/h2&gt;

&lt;p&gt;Access keys accept an expiry, set in days. Anything you create for a contractor, a demo, a spike or&lt;br&gt;
a one-off migration should have one, because the failure mode of temporary credentials is that they&lt;br&gt;
are never temporary. An expiry converts "someone should remember to delete this" into a guarantee. The&lt;br&gt;
dashboard warns you before a key expires, so the honest tradeoff is a scheduled, visible interruption&lt;br&gt;
against an unbounded credential that outlives the reason it was created. Long-lived production keys&lt;br&gt;
can stay open-ended; everything else should have a date on it.&lt;/p&gt;
&lt;h2&gt;
  
  
  Set a per-key rate cap
&lt;/h2&gt;

&lt;p&gt;Each access key can carry its own rate limit below your plan's ceiling. This is a stability feature&lt;br&gt;
more than a security one. A batch job that suddenly loops does not just fail; it consumes the whole&lt;br&gt;
account's per-minute budget and starves the user-facing service that shares it. Give the batch worker a&lt;br&gt;
cap it cannot exceed and the blast radius of that bug is the batch job alone. A useful default is to&lt;br&gt;
cap background workers at something well under half your plan limit and leave interactive traffic&lt;br&gt;
uncapped.&lt;/p&gt;
&lt;h2&gt;
  
  
  Rotate before you need to, and practise it
&lt;/h2&gt;

&lt;p&gt;Rotation issues a new secret for an existing key, keeping its name, scope, expiry and cap. That&lt;br&gt;
matters because it means rotating does not require rebuilding your configuration; only the secret&lt;br&gt;
changes.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;POST /auth/access-keys/{id}/rotate
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The reason to rehearse is that the hard part is never the rotate button. It is discovering, at the&lt;br&gt;
worst possible moment, that the key is hardcoded in a config file nobody has touched in a year, or that&lt;br&gt;
three services read it from an environment variable set by hand on a box that predates your deployment&lt;br&gt;
pipeline. Rotate one non-critical key deliberately, on a quiet afternoon, and find out how long it&lt;br&gt;
actually takes. Do that once and a real incident becomes a twenty-minute job instead of an evening.&lt;/p&gt;

&lt;p&gt;If a key is exposed publicly, in a repository, a client bundle or a log, rotate rather than delete.&lt;br&gt;
Rotation keeps the key's identity and settings while invalidating the leaked secret, so you get the&lt;br&gt;
security outcome without also losing the configuration.&lt;/p&gt;

&lt;h2&gt;
  
  
  Turn on 2FA for the account
&lt;/h2&gt;

&lt;p&gt;All of the above protects individual keys. None of it helps if someone signs in to the dashboard,&lt;br&gt;
because from there they can mint new keys at will. DietlyAPI supports TOTP two-factor authentication&lt;br&gt;
with any standard authenticator app, plus recovery codes for the day you lose your phone. Store the&lt;br&gt;
recovery codes somewhere that is not the same password manager you use to log in, and regenerate them&lt;br&gt;
if you ever use one. This is a two-minute setup that sits underneath every other control on this&lt;br&gt;
page.&lt;/p&gt;

&lt;h2&gt;
  
  
  Keep keys out of the places they leak from
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Client bundles.&lt;/strong&gt; Anything shipped to a browser or a mobile app is public, including&lt;br&gt;
values inlined at build time. Call the API from your server. The public read endpoints work with no key&lt;br&gt;
at all, so prototypes do not need one.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Repositories.&lt;/strong&gt; Use environment variables or a secret manager, and add a scanning&lt;br&gt;
hook so a key cannot be committed in the first place. Once a secret is in git history, rewriting the&lt;br&gt;
history is not a fix, because clones and forks still have it. Rotate.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Logs and error trackers.&lt;/strong&gt; Logging full request headers is the quiet one, because it&lt;br&gt;
usually works fine right up until the logs are shared with someone. Redact&lt;br&gt;
&lt;code&gt;Authorization&lt;/code&gt; at the logger.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Screenshots and support threads.&lt;/strong&gt; Keys get pasted into issue trackers constantly. If&lt;br&gt;
one has been, treat it as leaked and rotate; the cost of an unnecessary rotation is a few minutes.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Watch the audit log and the usage numbers
&lt;/h2&gt;

&lt;p&gt;The dashboard records account security events and shows per-key usage. Two habits make that data&lt;br&gt;
useful rather than decorative. First, look at per-key usage occasionally and confirm each key's shape&lt;br&gt;
matches what that service is supposed to do; a development key with steady round-the-clock traffic is&lt;br&gt;
telling you something. Second, check the audit log after anything unexpected, because it is the only&lt;br&gt;
record that distinguishes "our batch job misbehaved" from "someone else has account access". Both&lt;br&gt;
questions are answerable in about thirty seconds if you know where to look, and unanswerable if you&lt;br&gt;
have one shared key.&lt;/p&gt;

&lt;h2&gt;
  
  
  What good looks like
&lt;/h2&gt;

&lt;p&gt;A tidy small-team setup is four keys and one 2FA enrolment. A read-scope &lt;code&gt;web-prod&lt;/code&gt; key&lt;br&gt;
with no expiry and no per-key cap, serving user traffic. A read-scope &lt;code&gt;worker-prod&lt;/code&gt; key&lt;br&gt;
capped at a fraction of the plan limit, running scheduled syncs. A read-scope &lt;code&gt;ci&lt;/code&gt; key with a&lt;br&gt;
90-day expiry. A read-scope &lt;code&gt;laptop-dev&lt;/code&gt; key per developer, also expiring, and rotated&lt;br&gt;
without ceremony whenever anyone feels like it. Nothing in that list is difficult, and together they&lt;br&gt;
mean any single leak costs you one rotation and no downtime. If you also cache aggressively, as the&lt;br&gt;
&lt;a href="https://www.getdietly.com/developers/api-caching/" rel="noopener noreferrer"&gt;caching guide&lt;/a&gt; describes, the capped worker will rarely come close&lt;br&gt;
to its limit either.&lt;/p&gt;

&lt;h2&gt;
  
  
  Common questions
&lt;/h2&gt;

&lt;p&gt;What is the safest default scope for an API key?&lt;/p&gt;

&lt;p&gt;Read. A read-scope key can only perform GET requests, and anything else returns a 403 explaining that the key is read-only. Most integrations only look data up, and a leaked read-only key exposes catalog data that can already be queried anonymously.&lt;/p&gt;

&lt;p&gt;Should I rotate or delete a leaked key?&lt;/p&gt;

&lt;p&gt;Rotate. Rotation issues a new secret while keeping the key's name, scope, expiry and rate cap, so the leaked secret stops working without you having to rebuild the configuration around it.&lt;/p&gt;

&lt;p&gt;Why set a per-key rate limit?&lt;/p&gt;

&lt;p&gt;To contain bugs rather than attackers. A looping batch job on a shared key consumes the whole account's per-minute budget and starves user-facing traffic. A cap on the worker means the blast radius is the worker alone.&lt;/p&gt;

&lt;p&gt;Can I put an API key in my mobile app?&lt;/p&gt;

&lt;p&gt;No. Anything shipped in a client bundle is public, including values inlined at build time. Call the API from your own server. Prototypes can use the public read endpoints, which need no key at all.&lt;/p&gt;

</description>
      <category>security</category>
      <category>api</category>
      <category>cybersecurity</category>
      <category>webdev</category>
    </item>
    <item>
      <title>Caching Strategies for a Nutrition API: TTLs, Keys and the Cost Math</title>
      <dc:creator>Dietly</dc:creator>
      <pubDate>Wed, 22 Jul 2026 19:01:44 +0000</pubDate>
      <link>https://dev.to/dietly/caching-strategies-for-a-nutrition-api-ttls-keys-and-the-cost-math-387j</link>
      <guid>https://dev.to/dietly/caching-strategies-for-a-nutrition-api-ttls-keys-and-the-cost-math-387j</guid>
      <description>&lt;h1&gt;
  
  
  Caching strategies for nutrition APIs
&lt;/h1&gt;

&lt;p&gt;&lt;strong&gt;Nutrition data is close to the ideal caching workload: the records almost never&lt;br&gt;
change, the same few thousand foods account for most lookups, and a stale answer is rarely wrong.&lt;/strong&gt;&lt;br&gt;
A cache in front of DietlyAPI is therefore not an optimisation you add later under load. It is the&lt;br&gt;
difference between an app that fits comfortably inside the free 30 requests per minute and one that&lt;br&gt;
needs a paid plan to do the same job. This guide covers what to cache, for how long, how to key it, and&lt;br&gt;
the arithmetic that tells you which plan you actually need.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The short version:&lt;/strong&gt; cache barcode and food-id lookups for 30 days,&lt;br&gt;
search results for a day, and never cache a 429 or a 5xx. On a food-logging app that alone typically&lt;br&gt;
removes 80 to 95% of outbound calls, because users log the same foods repeatedly.&lt;/p&gt;
&lt;h2&gt;
  
  
  Pick a TTL per data type, not one for everything
&lt;/h2&gt;

&lt;p&gt;The right expiry follows from how often the underlying thing changes and what a stale answer costs&lt;br&gt;
you.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Response&lt;/th&gt;
&lt;th&gt;Suggested TTL&lt;/th&gt;
&lt;th&gt;Why&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;GET /barcode/{code}&lt;/code&gt; hit&lt;/td&gt;
&lt;td&gt;30 days&lt;/td&gt;
&lt;td&gt;A barcode maps to one product; reformulations are rare and slow&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;GET /food/{id}&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;30 days&lt;/td&gt;
&lt;td&gt;Same record, addressed directly; the safest thing in the catalog to cache&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;GET /search?q=...&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;1 to 7 days&lt;/td&gt;
&lt;td&gt;Ranking and catalog coverage improve over time, so let it refresh&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;404 from a lookup&lt;/td&gt;
&lt;td&gt;1 to 24 hours&lt;/td&gt;
&lt;td&gt;Negative caching stops a scanner hammering the same unknown barcode&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;429 or any 5xx&lt;/td&gt;
&lt;td&gt;Never&lt;/td&gt;
&lt;td&gt;Caching a failure turns a blip into an outage you inflicted&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Negative caching deserves the emphasis. A barcode scanner pointed at an unlisted product will retry&lt;br&gt;
on every frame if you let it, and each one of those is a real request against your limit. Caching the&lt;br&gt;
404 for even an hour converts a pathological loop into a single call, and the user experience is&lt;br&gt;
identical because the answer is the same either way.&lt;/p&gt;
&lt;h2&gt;
  
  
  Key the cache on what actually varies
&lt;/h2&gt;

&lt;p&gt;A cache key should contain everything that changes the response and nothing that does not. For this&lt;br&gt;
API that is the endpoint, the identifier or normalised query, and the limit.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;cache_key&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;kind&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;value&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;limit&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;kind&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;search&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="c1"&gt;# normalise so "Greek Yogurt", "greek yogurt " and "greek  yogurt"
&lt;/span&gt;        &lt;span class="c1"&gt;# are one entry rather than three
&lt;/span&gt;        &lt;span class="n"&gt;value&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt; &lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;join&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;value&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;lower&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;split&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;dietly:&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;kind&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;:&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;value&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;:&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;limit&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;limit&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="sh"&gt;""&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="nf"&gt;cache_key&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;search&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;  Greek   Yogurt &lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;   &lt;span class="c1"&gt;# dietly:search:greek yogurt:10
&lt;/span&gt;&lt;span class="nf"&gt;cache_key&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;barcode&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;5000159407236&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;          &lt;span class="c1"&gt;# dietly:barcode:5000159407236
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Two mistakes are worth avoiding. Do not put the API key in the cache key: the response does not&lt;br&gt;
depend on which key fetched it, and including it fragments the cache per user for no benefit. Do put&lt;br&gt;
the &lt;code&gt;limit&lt;/code&gt; in, because a request for 5 results and one for 50 genuinely differ, and serving&lt;br&gt;
the short list to the caller who asked for the long one is a bug that will take you an afternoon to&lt;br&gt;
find.&lt;/p&gt;
&lt;h2&gt;
  
  
  A cache that fits in twenty lines
&lt;/h2&gt;

&lt;p&gt;You do not need Redis to start. A dictionary with timestamps handles a single process, and the&lt;br&gt;
interface is the same one you will keep when you move to something shared.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;time&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;requests&lt;/span&gt;

&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;CachedDietly&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;__init__&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;ttl&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;60&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;60&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;24&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;30&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;api_key&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ttl&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;ttl&lt;/span&gt;
        &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;store&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{}&lt;/span&gt;
        &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;session&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;requests&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Session&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;api_key&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;session&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="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Authorization&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Bearer &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;api_key&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;

    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;_get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;key&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;url&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;params&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;ttl&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="n"&gt;ttl&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ttl&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;ttl&lt;/span&gt; &lt;span class="ow"&gt;is&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="n"&gt;ttl&lt;/span&gt;
        &lt;span class="n"&gt;hit&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;store&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="n"&gt;key&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;hit&lt;/span&gt; &lt;span class="ow"&gt;and&lt;/span&gt; &lt;span class="n"&gt;time&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;time&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;hit&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="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="n"&gt;ttl&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;hit&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;1&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;=&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;session&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="n"&gt;url&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;params&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;params&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;timeout&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;status_code&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="mi"&gt;404&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;store&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;key&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;time&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;time&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;      &lt;span class="c1"&gt;# negative cache
&lt;/span&gt;            &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="bp"&gt;None&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;raise_for_status&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;                           &lt;span class="c1"&gt;# never cache 429/5xx
&lt;/span&gt;        &lt;span class="n"&gt;data&lt;/span&gt; &lt;span class="o"&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="c1"&gt;# parse once, not twice
&lt;/span&gt;        &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;store&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;key&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;time&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;time&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="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;data&lt;/span&gt;

    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;barcode&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;code&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;self&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="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;barcode:&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;code&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                         &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;https://api.getdietly.com/barcode/&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;code&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;search&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;q&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;limit&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="n"&gt;q&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt; &lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;join&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;q&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;lower&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;split&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;self&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="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;search:&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;q&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;:&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;limit&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                         &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;https://api.getdietly.com/search&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                         &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;q&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;q&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;limit&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;limit&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;
                         &lt;span class="n"&gt;ttl&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;60&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;60&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;24&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The important line is &lt;code&gt;r.raise_for_status()&lt;/code&gt; sitting &lt;em&gt;above&lt;/em&gt; the store write.&lt;br&gt;
Failures must never enter the cache. The second important line is the negative cache on 404, which&lt;br&gt;
stores &lt;code&gt;None&lt;/code&gt; rather than skipping the write, so a repeated miss costs nothing.&lt;/p&gt;
&lt;h2&gt;
  
  
  Move it out of process when you have more than one
&lt;/h2&gt;

&lt;p&gt;An in-process dictionary dies with the worker and is duplicated across every replica, so four&lt;br&gt;
workers means four cold caches and four times the upstream traffic. Redis with a TTL fixes both.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;json&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;redis&lt;/span&gt;

&lt;span class="n"&gt;r&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;redis&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Redis&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;cached_json&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;key&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;ttl&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;fetch&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;raw&lt;/span&gt; &lt;span class="o"&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;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;key&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;raw&lt;/span&gt; &lt;span class="ow"&gt;is&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;loads&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;raw&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;value&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;fetch&lt;/span&gt;&lt;span class="p"&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;setex&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;key&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;ttl&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;dumps&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;value&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;value&lt;/span&gt;

&lt;span class="n"&gt;food&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;cached_json&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;dietly:barcode:5000159407236&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;60&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;60&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;24&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;30&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                   &lt;span class="k"&gt;lambda&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;requests&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="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;https://api.getdietly.com/barcode/5000159407236&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                       &lt;span class="n"&gt;timeout&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;10&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;If your app is a web front end, the browser can carry part of the load too. Barcode and food-id&lt;br&gt;
responses are safe to cache in the client for a day, and doing so removes the network round trip&lt;br&gt;
entirely for a user scrolling back through their own food log. Search is the one to leave uncached in&lt;br&gt;
the browser, because a stale result list is more visible than a stale nutrient panel.&lt;/p&gt;

&lt;h2&gt;
  
  
  Do not put the key in the browser
&lt;/h2&gt;

&lt;p&gt;Caching pushes people toward calling the API from client code, which is where keys leak. Anything&lt;br&gt;
shipped to a browser or a mobile binary is public, including values in environment variables at build&lt;br&gt;
time. Call the API from your own server, cache there, and let your front end talk to your endpoint. The&lt;br&gt;
public read endpoints work without a key, so a prototype can genuinely run key-free from the browser at&lt;br&gt;
30 requests per minute per IP, and that is a reasonable way to build a demo. Ship it with a key in it&lt;br&gt;
and you have published the key. The &lt;a href="https://www.getdietly.com/developers/api-key-security/" rel="noopener noreferrer"&gt;key hygiene guide&lt;/a&gt;&lt;br&gt;
covers rotation if that has already happened.&lt;/p&gt;

&lt;h2&gt;
  
  
  The arithmetic that decides your plan
&lt;/h2&gt;

&lt;p&gt;Work in requests per minute at peak, not per month, because the limit is per minute. Suppose a&lt;br&gt;
food-logging app with 2,000 daily active users, each logging six items, and a peak hour carrying a fifth&lt;br&gt;
of the day's traffic. That is 12,000 lookups a day, 2,400 in the peak hour, or 40 per minute: over the&lt;br&gt;
30 per minute free ceiling, so uncached you need a paid plan. Now add a cache. Food logging is&lt;br&gt;
extremely repetitive, so a 90% hit rate is conservative once the cache is warm, and 40 per minute&lt;br&gt;
becomes 4 per minute. The same app fits in the free tier with room to spare, and every cached response&lt;br&gt;
is also faster than the fastest possible network call.&lt;/p&gt;

&lt;p&gt;That is the honest version of the advice, so here is the equally honest limit of it. Caching does not&lt;br&gt;
help a workload whose keys are nearly all unique, such as a one-off bulk import of a product catalog&lt;br&gt;
you have never seen before. For that case, pace the import against &lt;code&gt;X-RateLimit-Remaining&lt;/code&gt;,&lt;br&gt;
run it once, and store the results in your own database rather than repeatedly asking. If you need&lt;br&gt;
millions of rows, ask about a bulk export instead of crawling the API for a week; that is a cheaper&lt;br&gt;
conversation for everyone.&lt;/p&gt;

&lt;h2&gt;
  
  
  Measure the hit rate, or you are guessing
&lt;/h2&gt;

&lt;p&gt;Count hits and misses and log the ratio. A hit rate below 50% on a consumer food app almost always&lt;br&gt;
means the cache key is wrong, usually because an unnormalised search string, a per-user prefix or a&lt;br&gt;
timestamp has crept into it. A hit rate above 95% means you can extend TTLs further and stop thinking&lt;br&gt;
about the rate limit at all. Both numbers are cheap to collect and neither is guessable from&lt;br&gt;
first principles.&lt;/p&gt;

&lt;h2&gt;
  
  
  Common questions
&lt;/h2&gt;

&lt;p&gt;How long should I cache nutrition API responses?&lt;/p&gt;

&lt;p&gt;Barcode and food-id lookups for around 30 days, since a barcode maps to one product and reformulations are rare. Search results for one to seven days so improved ranking and coverage reach your users. Never cache a 429 or a 5xx.&lt;/p&gt;

&lt;p&gt;Should I cache 404 responses?&lt;/p&gt;

&lt;p&gt;Yes, for one to twenty-four hours. A barcode scanner pointed at an unlisted product will otherwise retry on every frame, and each retry is a real request against your rate limit for an answer that will not change.&lt;/p&gt;

&lt;p&gt;Can caching keep me on the free tier?&lt;/p&gt;

&lt;p&gt;Often. A food-logging app doing 40 lookups per minute at peak exceeds the 30 per minute free limit, but food logging is repetitive enough that a 90% cache hit rate is conservative, which brings it to about 4 per minute.&lt;/p&gt;

&lt;p&gt;Can I call the API directly from the browser?&lt;/p&gt;

&lt;p&gt;Only without a key. The public read endpoints work anonymously at 30 requests per minute per IP, which is fine for a prototype. Anything shipped to a browser or a mobile binary is public, so a key in client code is a published key.&lt;/p&gt;

</description>
      <category>api</category>
      <category>python</category>
      <category>performance</category>
      <category>webdev</category>
    </item>
    <item>
      <title>Every DietlyAPI Error Code Explained, With Handling Recipes</title>
      <dc:creator>Dietly</dc:creator>
      <pubDate>Wed, 22 Jul 2026 19:01:43 +0000</pubDate>
      <link>https://dev.to/dietly/every-dietlyapi-error-code-explained-with-handling-recipes-505b</link>
      <guid>https://dev.to/dietly/every-dietlyapi-error-code-explained-with-handling-recipes-505b</guid>
      <description>&lt;h1&gt;
  
  
  Every DietlyAPI error explained, with handling recipes
&lt;/h1&gt;

&lt;p&gt;&lt;strong&gt;DietlyAPI returns five error statuses in normal operation: 401, 403, 404,&lt;br&gt;
422 and 429. Four of them carry &lt;code&gt;detail&lt;/code&gt; as a string, and 422 carries&lt;br&gt;
&lt;code&gt;detail&lt;/code&gt; as a list of objects.&lt;/strong&gt; That single shape change is the most common cause&lt;br&gt;
of a crash in an otherwise working integration, because a handler written as&lt;br&gt;
&lt;code&gt;err["detail"].lower()&lt;/code&gt; works perfectly until the first validation error. This page lists&lt;br&gt;
every status the API produces, the exact body it sends, what causes it, and the handling that belongs&lt;br&gt;
with it. Every example below was run against the live API.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Status&lt;/th&gt;
&lt;th&gt;Body&lt;/th&gt;
&lt;th&gt;Retry?&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;401&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;{"detail": "Missing or invalid API key"}&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;No, fix the request&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;403&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;{"detail": "This API key is read-only. ..."}&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;No, use a different key&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;404&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;{"detail": "Barcode not found"}&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;No, treat as empty state&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;422&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;{"detail": [ {...} ]}&lt;/code&gt;, a list&lt;/td&gt;
&lt;td&gt;No, fix the parameters&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;429&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;{"detail": "Rate limit exceeded. Retry in N seconds."}&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Yes, after &lt;code&gt;Retry-After&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;
&lt;h2&gt;
  
  
  401: missing or invalid API key
&lt;/h2&gt;

&lt;p&gt;The API expects a bearer token: &lt;code&gt;Authorization: Bearer YOUR_KEY&lt;/code&gt;. You get a 401 when the&lt;br&gt;
header is absent, when the scheme is not &lt;code&gt;Bearer&lt;/code&gt;, or when the key does not exist. The body&lt;br&gt;
is deliberately identical in all three cases so the endpoint cannot be used to probe which keys are&lt;br&gt;
real.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;$ curl -s https://api.getdietly.com/nope
{"detail":"Missing or invalid API key"}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;There is one non-obvious cause worth knowing, and it is the reason that example uses a nonsense&lt;br&gt;
path. &lt;strong&gt;Authentication runs before routing, so a URL that does not exist returns 401 rather than&lt;br&gt;
404.&lt;/strong&gt; If you are certain your key is valid and you are still getting 401, check the path before&lt;br&gt;
you check the key. The classic version of this mistake is inserting a version prefix:&lt;br&gt;
&lt;code&gt;/v1/search&lt;/code&gt; returns 401 because there is no &lt;code&gt;/v1&lt;/code&gt; namespace, while&lt;br&gt;
&lt;code&gt;/search&lt;/code&gt; works. The public read endpoints (&lt;code&gt;/search&lt;/code&gt;, &lt;code&gt;/barcode/{code}&lt;/code&gt;,&lt;br&gt;
&lt;code&gt;/food/{id}&lt;/code&gt;) need no key at all, so a 401 from one of those is nearly always a typo in the&lt;br&gt;
path.&lt;/p&gt;
&lt;h2&gt;
  
  
  403: the key exists but is not allowed to do this
&lt;/h2&gt;

&lt;p&gt;A 403 means authentication succeeded and authorisation failed. There are two forms, and the message&lt;br&gt;
tells you which.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;{"detail":"This API key is read-only. Use a full-access key for this endpoint."}
{"detail":"Use your account key to manage dashboard settings."}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The first appears when a key created with read scope is used for anything that is not a GET. Read&lt;br&gt;
scope is the right default for anything you deploy to a server that only looks data up, and the&lt;br&gt;
restriction is the point rather than a limitation. The second appears when a named access key is used&lt;br&gt;
against &lt;code&gt;/auth/&lt;/code&gt; or &lt;code&gt;/billing/&lt;/code&gt; routes; account management uses the account key,&lt;br&gt;
not the per-service keys. Neither is retryable. Both are configuration mistakes that surface once, at&lt;br&gt;
integration time, and never again.&lt;/p&gt;
&lt;h2&gt;
  
  
  404: no such record
&lt;/h2&gt;

&lt;p&gt;Direct lookups return 404 when nothing matches: &lt;code&gt;GET /food/{id}&lt;/code&gt; for an unknown id, and&lt;br&gt;
&lt;code&gt;GET /barcode/{code}&lt;/code&gt; for a barcode with no product. This is an ordinary outcome in a&lt;br&gt;
barcode scanner, not a failure, and it should never surface to a user as an error dialog.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;$ curl -s https://api.getdietly.com/barcode/5099999999999
{"detail":"Barcode not found"}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Note the asymmetry with search: &lt;code&gt;GET /search&lt;/code&gt; with no matches returns &lt;code&gt;200&lt;/code&gt;&lt;br&gt;
and an empty array &lt;code&gt;[]&lt;/code&gt;, not a 404. Search answers "what matched", which can legitimately be&lt;br&gt;
nothing; a direct lookup answers "give me this record", which either exists or does not. Handle both,&lt;br&gt;
and treat both as the same empty state in your UI.&lt;/p&gt;
&lt;h2&gt;
  
  
  422: your parameters did not validate
&lt;/h2&gt;

&lt;p&gt;This is the one that breaks parsers. On 422, &lt;code&gt;detail&lt;/code&gt; is a &lt;em&gt;list&lt;/em&gt; of validation&lt;br&gt;
objects, each with &lt;code&gt;type&lt;/code&gt;, &lt;code&gt;loc&lt;/code&gt;, &lt;code&gt;msg&lt;/code&gt; and &lt;code&gt;input&lt;/code&gt;. Every&lt;br&gt;
other error sends &lt;code&gt;detail&lt;/code&gt; as a plain string.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;$ curl -s "https://api.getdietly.com/search?q=a"
{"detail":[{"type":"string_too_short","loc":["query","q"],
            "msg":"String should have at least 2 characters",
            "input":"a","ctx":{"min_length":2}}]}

$ curl -s "https://api.getdietly.com/search?q=oats&amp;amp;limit=999"
{"detail":[{"type":"less_than_equal","loc":["query","limit"],
            "msg":"Input should be less than or equal to 50","input":"999"}]}

$ curl -s https://api.getdietly.com/food/abc
{"detail":[{"type":"int_parsing","loc":["path","food_id"],
            "msg":"Input should be a valid integer, unable to parse string as an integer",
            "input":"abc"}]}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The three rules worth encoding in your client: &lt;code&gt;q&lt;/code&gt; is required and needs at least two&lt;br&gt;
characters, &lt;code&gt;limit&lt;/code&gt; must be between 1 and 50, and a food id must be an integer. Validate&lt;br&gt;
those before you send, because a 422 still costs you a round trip. In particular, do not pass a raw&lt;br&gt;
search box through on every keystroke: a single-character query is guaranteed to 422, so debounce and&lt;br&gt;
require two characters locally.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;describe_error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="c1"&gt;# Return a readable message for any DietlyAPI error body.
&lt;/span&gt;    &lt;span class="k"&gt;try&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;detail&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;response&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="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;detail&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;except&lt;/span&gt; &lt;span class="nb"&gt;ValueError&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;HTTP &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;status_code&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="nf"&gt;isinstance&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;detail&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;list&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;                      &lt;span class="c1"&gt;# 422 validation
&lt;/span&gt;        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;; &lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;join&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;.&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;join&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;str&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;p&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;item&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="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;loc&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;[]))&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;item&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="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;msg&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
            &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;item&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;detail&lt;/span&gt;
        &lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;str&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;detail&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;                                &lt;span class="c1"&gt;# 401, 403, 404, 429
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  429: rate limited
&lt;/h2&gt;

&lt;p&gt;The limit is per minute. Anonymous and Starter traffic is capped at 30 requests per minute per IP,&lt;br&gt;
with an additional hourly cap that only a scraper would reach; Pro is 500 per minute and Scale 3,000&lt;br&gt;
per minute, both counted per account rather than per IP. When you exceed it you get a 429 with a&lt;br&gt;
&lt;code&gt;Retry-After&lt;/code&gt; header in seconds.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;{"detail":"Rate limit exceeded. Retry in 12 seconds."}
Retry-After: 12
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Requests made with a key also carry the full trio on both success and 429:&lt;br&gt;
&lt;code&gt;X-RateLimit-Limit&lt;/code&gt;, &lt;code&gt;X-RateLimit-Remaining&lt;/code&gt; and &lt;code&gt;X-RateLimit-Reset&lt;/code&gt;.&lt;br&gt;
Anonymous traffic gets &lt;code&gt;Retry-After&lt;/code&gt; only. If you are building a dashboard or a bulk sync,&lt;br&gt;
read &lt;code&gt;X-RateLimit-Remaining&lt;/code&gt; on every response and slow down before you hit zero rather than&lt;br&gt;
after. That is the difference between a sync that finishes and one that spends its life in backoff.&lt;/p&gt;
&lt;h2&gt;
  
  
  A retry policy that fits these semantics
&lt;/h2&gt;

&lt;p&gt;Only two of the five statuses are retryable, and one of them tells you exactly how long to wait.&lt;br&gt;
Everything else is a bug in your request that no amount of retrying will fix, which makes the policy&lt;br&gt;
short.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;time&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;requests&lt;/span&gt;

&lt;span class="n"&gt;RETRYABLE&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="mi"&gt;429&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;500&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;502&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;503&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;504&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;request_with_retry&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;session&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="n"&gt;url&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;tries&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;4&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;**&lt;/span&gt;&lt;span class="n"&gt;kwargs&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;delay&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mf"&gt;1.0&lt;/span&gt;
    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;attempt&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="nf"&gt;range&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;tries&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;=&lt;/span&gt; &lt;span class="n"&gt;session&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;request&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="n"&gt;url&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;timeout&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;**&lt;/span&gt;&lt;span class="n"&gt;kwargs&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;status_code&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;RETRYABLE&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;status_code&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="mi"&gt;429&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="n"&gt;wait&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;float&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;r&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="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Retry-After&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;delay&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
        &lt;span class="k"&gt;else&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="n"&gt;wait&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;delay&lt;/span&gt;
            &lt;span class="n"&gt;delay&lt;/span&gt; &lt;span class="o"&gt;*=&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;                      &lt;span class="c1"&gt;# exponential backoff for 5xx only
&lt;/span&gt;        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;attempt&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="n"&gt;tries&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt;
        &lt;span class="n"&gt;time&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;sleep&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;wait&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Three details make this behave well in production. Honour &lt;code&gt;Retry-After&lt;/code&gt; literally instead&lt;br&gt;
of applying your own backoff on top of it, because the server already knows when the window resets.&lt;br&gt;
Back off exponentially only for 5xx, where you have no information. And never retry 401, 403, 404 or&lt;br&gt;
422: a retry loop on a 422 is an infinite loop that also burns your rate limit.&lt;/p&gt;

&lt;h2&gt;
  
  
  Deciding what is an error in your own domain
&lt;/h2&gt;

&lt;p&gt;The status code tells you what happened at the transport level. Your application still has to decide&lt;br&gt;
what it means. A useful split is three buckets. &lt;strong&gt;Empty results&lt;/strong&gt; covers 404 on a lookup&lt;br&gt;
and &lt;code&gt;[]&lt;/code&gt; from a search, and belongs in the UI as "no match found", with an option to submit&lt;br&gt;
the product. &lt;strong&gt;Caller bugs&lt;/strong&gt; covers 401, 403 and 422, and belongs in your logs and your&lt;br&gt;
tests, never in front of a user. &lt;strong&gt;Transient&lt;/strong&gt; covers 429 and 5xx, and belongs in the retry&lt;br&gt;
path with a visible spinner rather than an error. Fold the first bucket into normal flow and most of&lt;br&gt;
the perceived unreliability of any API disappears.&lt;/p&gt;

&lt;p&gt;One more thing worth handling explicitly: nutrients can be &lt;code&gt;null&lt;/code&gt; on a perfectly&lt;br&gt;
successful 200. A label that did not declare fibre gives you &lt;code&gt;"fiber_g": null&lt;/code&gt;. Treat that&lt;br&gt;
as unknown, never as zero, because rendering a confident 0 g for an undeclared nutrient is a data&lt;br&gt;
error your users will notice and the API will get blamed for.&lt;/p&gt;

&lt;h2&gt;
  
  
  Before you file a bug
&lt;/h2&gt;

&lt;p&gt;Check the status page first: if the API is degraded, retries are the answer rather than code&lt;br&gt;
changes. Then reproduce with &lt;code&gt;curl&lt;/code&gt; and no key against a public read endpoint, because that&lt;br&gt;
removes authentication, your HTTP client and your framework from the picture in one step. If a plain&lt;br&gt;
curl works and your client does not, the difference is in your request, and the response body will&lt;br&gt;
usually name the parameter.&lt;/p&gt;

&lt;h2&gt;
  
  
  Common questions
&lt;/h2&gt;

&lt;p&gt;Why am I getting a 401 when my API key is valid?&lt;/p&gt;

&lt;p&gt;Because authentication runs before routing, so a path that does not exist returns 401 rather than 404. The most common cause is adding a version prefix: /v1/search returns 401 while /search works. The public read endpoints need no key at all.&lt;/p&gt;

&lt;p&gt;Why does my error handler crash on 422?&lt;/p&gt;

&lt;p&gt;On 422 the detail field is a list of validation objects, while every other DietlyAPI error sends detail as a plain string. Code written as detail.lower() works until the first validation error and then raises.&lt;/p&gt;

&lt;p&gt;What are the DietlyAPI rate limits?&lt;/p&gt;

&lt;p&gt;Anonymous and Starter traffic gets 30 requests per minute per IP plus an hourly anti-scraping cap. Pro is 500 per minute and Scale 3,000 per minute, both counted per account. Exceeding the limit returns 429 with a Retry-After header in seconds.&lt;/p&gt;

&lt;p&gt;Does an empty search return 404?&lt;/p&gt;

&lt;p&gt;No. GET /search with no matches returns 200 and an empty array. Only direct lookups, GET /food/{id} and GET /barcode/{code}, return 404 when the record does not exist.&lt;/p&gt;

</description>
      <category>api</category>
      <category>python</category>
      <category>webdev</category>
      <category>programming</category>
    </item>
    <item>
      <title>Contract Testing a Nutrition API with Millions of Messy Records</title>
      <dc:creator>Dietly</dc:creator>
      <pubDate>Sun, 19 Jul 2026 14:42:09 +0000</pubDate>
      <link>https://dev.to/dietly/contract-testing-a-nutrition-api-with-millions-of-messy-records-6bc</link>
      <guid>https://dev.to/dietly/contract-testing-a-nutrition-api-with-millions-of-messy-records-6bc</guid>
      <description>&lt;h1&gt;
  
  
  Contract testing a nutrition API with millions of messy records
&lt;/h1&gt;

&lt;p&gt;&lt;strong&gt;A schema can remain valid while every client still breaks.&lt;/strong&gt; Changing search from a JSON array to &lt;code&gt;{"results": [...]}&lt;/code&gt;, converting &lt;code&gt;null&lt;/code&gt; to zero, or renaming &lt;code&gt;carbs_g&lt;/code&gt; is enough to break a mobile release that cannot update immediately.&lt;/p&gt;

&lt;h2&gt;
  
  
  Freeze the consumer-visible shape
&lt;/h2&gt;

&lt;p&gt;An OpenAPI document is necessary, but a small executable fixture catches accidental differences between the document and the running service. Store one representative response and assert keys, types and nullability.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;EXPECTED_KEYS&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;id&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;name&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;brand&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;barcode&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;category&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;serving_size_g&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;serving_desc&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;calories_kcal&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;protein_g&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;fat_g&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;carbs_g&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;fiber_g&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;sugar_g&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;sodium_mg&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;source&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;confidence&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;static_url&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;test_food_contract&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;client&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;food&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;client&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="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;/food/1068319&lt;/span&gt;&lt;span class="sh"&gt;"&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;assert&lt;/span&gt; &lt;span class="n"&gt;EXPECTED_KEYS&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;=&lt;/span&gt; &lt;span class="n"&gt;food&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;keys&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="k"&gt;assert&lt;/span&gt; &lt;span class="nf"&gt;isinstance&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;food&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;id&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="nb"&gt;int&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;assert&lt;/span&gt; &lt;span class="n"&gt;food&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;brand&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="ow"&gt;is&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt; &lt;span class="ow"&gt;or&lt;/span&gt; &lt;span class="nf"&gt;isinstance&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;food&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;brand&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Test the envelope separately
&lt;/h2&gt;

&lt;p&gt;The most damaging change is often outside the object. Assert that search returns a bare array if that is the published contract, a food lookup returns one object, and a missing ID or barcode returns 404.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;test_endpoint_envelopes&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;client&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="k"&gt;assert&lt;/span&gt; &lt;span class="nf"&gt;isinstance&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;client&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="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;/search?q=yogurt&lt;/span&gt;&lt;span class="sh"&gt;"&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="nb"&gt;list&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;assert&lt;/span&gt; &lt;span class="nf"&gt;isinstance&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;client&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="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;/food/1068319&lt;/span&gt;&lt;span class="sh"&gt;"&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="nb"&gt;dict&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;assert&lt;/span&gt; &lt;span class="n"&gt;client&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="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;/barcode/00000000&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="n"&gt;status_code&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="mi"&gt;404&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Nullability deserves dedicated fixtures
&lt;/h2&gt;

&lt;p&gt;Clean example data hides real failures. Keep fixtures for a complete branded product, a sparse community record, a zero-calorie item, a missing serving size, Unicode text and a duplicate barcode. Verify that unknown nutrients remain &lt;code&gt;null&lt;/code&gt; while reported zero stays numeric zero.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Fixture&lt;/th&gt;
&lt;th&gt;Regression it catches&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Sparse record&lt;/td&gt;
&lt;td&gt;Null coerced to zero or field omitted&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Water/zero-calorie item&lt;/td&gt;
&lt;td&gt;Falsy zero treated as missing&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Unicode brand&lt;/td&gt;
&lt;td&gt;Encoding and normalization damage&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Duplicate barcode&lt;/td&gt;
&lt;td&gt;Unstable winner ordering&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Missing barcode&lt;/td&gt;
&lt;td&gt;Invalid uniqueness assumptions&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Large serving&lt;/td&gt;
&lt;td&gt;Unit and range mistakes&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  Separate contract tests from ranking tests
&lt;/h2&gt;

&lt;p&gt;A contract test asks whether clients can parse the response. A ranking test asks whether useful foods appear in the right order. Freeze a set of queries and expected top IDs or relevance bands, but do not make every exact rank immutable: data refreshes legitimately add products.&lt;/p&gt;

&lt;h2&gt;
  
  
  Probe production read-only
&lt;/h2&gt;

&lt;p&gt;Local tests cannot prove that the deployed reverse proxy, database and serializer agree. Run a small read-only probe after deployment: health, one search, one food ID, one known barcode, one 404 and one rate-limit header check. Never mutate community data during a smoke test.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;test_live_search_contract&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;session&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;base&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;=&lt;/span&gt; &lt;span class="n"&gt;session&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="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;base&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;/search&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;params&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;q&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;oat milk&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;limit&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&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;raise_for_status&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="k"&gt;assert&lt;/span&gt; &lt;span class="n"&gt;r&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="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;content-type&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nf"&gt;startswith&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;application/json&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;assert&lt;/span&gt; &lt;span class="nf"&gt;len&lt;/span&gt;&lt;span class="p"&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="o"&gt;&amp;lt;=&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Classify changes before shipping
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Additive:&lt;/strong&gt; a new nullable field is usually safe.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Behavioral:&lt;/strong&gt; ranking or rate-limit changes need release notes and tests.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Breaking:&lt;/strong&gt; renamed fields, changed types or envelopes require a version or migration window.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Data:&lt;/strong&gt; corrected values should not require a schema version, but may affect snapshots.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Keep a frozen production-baseline JSON file in the repository and review diffs. That turns “the API probably stayed compatible” into evidence.&lt;/p&gt;

&lt;h2&gt;
  
  
  Use producer tests and consumer tests together
&lt;/h2&gt;

&lt;p&gt;Producer-side tests verify that the API implementation follows its declared schema. Consumer-driven tests capture assumptions made by real clients: search is an array, a particular header exists, unknown sugar remains null and a 404 body can be parsed. Neither view is sufficient alone. The server may satisfy OpenAPI while changing an undocumented behavior on which every released mobile client depends.&lt;/p&gt;

&lt;p&gt;Collect consumer expectations deliberately rather than recording all current behavior forever. Protect the parts required for compatibility, and allow internal implementation details to change. Give each expectation an owner and an explanation so obsolete constraints can be retired safely.&lt;/p&gt;

&lt;h2&gt;
  
  
  Avoid brittle full-response snapshots
&lt;/h2&gt;

&lt;p&gt;Nutrition values and product names legitimately change as upstream records are corrected. A snapshot of an entire live response will create noisy failures and encourage developers to approve changes without reading them. Assert schema, invariants and a few controlled fixtures instead. When values matter, seed a test database with records owned by the test suite.&lt;/p&gt;

&lt;p&gt;Useful invariants include nutrient values being numeric or null, IDs being positive integers, result limits being respected, confidence staying in its documented range and barcode misses returning 404 rather than a fabricated object. Property-based tests can generate combinations of nullable fields and serving sizes that hand-written examples overlook.&lt;/p&gt;

&lt;h2&gt;
  
  
  Version data contracts independently from deployments
&lt;/h2&gt;

&lt;p&gt;A server can deploy every day while its public contract remains version one. Track contract changes in release notes and compare the generated OpenAPI document to a reviewed baseline in continuous integration. A diff that removes a property, narrows a type or changes required fields should fail until someone classifies it.&lt;/p&gt;

&lt;p&gt;When a breaking change is necessary, prefer an explicit endpoint or media-type version and run both contracts during a migration window. Monitor which version clients use before retiring the old one. Announcing a date is not enough if telemetry shows active clients cannot upgrade.&lt;/p&gt;

&lt;h2&gt;
  
  
  Test operational behavior as part of the contract
&lt;/h2&gt;

&lt;p&gt;Rate-limit responses, authentication failures, cache headers and content types affect integrations as much as JSON fields. Verify that 429 includes a useful &lt;code&gt;Retry-After&lt;/code&gt;, that anonymous endpoints remain anonymous if promised, and that privileged keys never appear in logs or error bodies. Confirm that proxies preserve status codes instead of converting every failure into HTML.&lt;/p&gt;

&lt;p&gt;Run probes from outside the origin network. An internal health endpoint can be green while DNS, TLS or the CDN is failing for customers. Keep production probes small, read-only and rate-aware, and distinguish origin processing time from customer-visible end-to-end latency.&lt;/p&gt;

&lt;h2&gt;
  
  
  Make compatibility review part of code review
&lt;/h2&gt;

&lt;p&gt;When a pull request touches response models, serializers, SQL column aliases or middleware, require a contract impact note. Reviewers should see the before-and-after schema and relevant fixture changes. Generated clients can be compiled in CI to expose changes that look harmless in JSON but break a strongly typed language.&lt;/p&gt;

&lt;p&gt;The goal is not to prevent evolution. It is to make breaking changes intentional, observable and survivable. A boring stable contract is a product feature when customers build apps that outlive your latest deployment.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Dietly contract:&lt;/strong&gt; search remains a bare JSON array; nutrient fields retain nullability; ID and barcode lookups return one object or 404. The &lt;a href="https://www.getdietly.com/openapi.json" rel="noopener noreferrer"&gt;OpenAPI specification&lt;/a&gt; documents the public shape.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Originally published at &lt;a href="https://www.getdietly.com/developers/contract-testing-nutrition-api/" rel="noopener noreferrer"&gt;getdietly.com&lt;/a&gt;. Data from the &lt;a href="https://www.getdietly.com/api" rel="noopener noreferrer"&gt;Dietly Nutrition API&lt;/a&gt; — 4.7M+ indexed foods, free tier available.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>testing</category>
      <category>api</category>
      <category>python</category>
      <category>webdev</category>
    </item>
    <item>
      <title>What ODbL Means for Commercial Nutrition Apps</title>
      <dc:creator>Dietly</dc:creator>
      <pubDate>Sun, 19 Jul 2026 14:41:48 +0000</pubDate>
      <link>https://dev.to/dietly/what-odbl-means-for-commercial-nutrition-apps-4hl7</link>
      <guid>https://dev.to/dietly/what-odbl-means-for-commercial-nutrition-apps-4hl7</guid>
      <description>&lt;h1&gt;
  
  
  What ODbL means for commercial nutrition apps
&lt;/h1&gt;

&lt;p&gt;&lt;strong&gt;This is an engineering overview, not legal advice.&lt;/strong&gt; Database rights and contract questions depend on jurisdiction and architecture. Read the licence and obtain qualified advice for a consequential launch.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The Open Database License permits commercial use.&lt;/strong&gt; It also attaches conditions to public use, attribution and certain distributions of databases derived from the licensed database. Paying a hosted provider does not automatically remove those upstream obligations.&lt;/p&gt;

&lt;h2&gt;
  
  
  Separate the database from its contents
&lt;/h2&gt;

&lt;p&gt;ODbL governs rights in the database: its systematic collection, arrangement and substantial extraction or reuse. Individual contents can have separate rights. Open Food Facts states that its database is under ODbL, individual database contents use the Database Contents License, and product images use Creative Commons Attribution-ShareAlike, with possible additional rights in packaging.&lt;/p&gt;

&lt;p&gt;This distinction matters. Permission to query a nutrition record does not necessarily grant unrestricted rights to every product photograph or logo returned with it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Commercial use is allowed
&lt;/h2&gt;

&lt;p&gt;Section 3 of ODbL grants worldwide rights that explicitly include commercial use. “Open” does not mean “non-commercial.” The practical questions are what you publicly display, whether you distribute a database, and whether your transformed collection is a derivative database.&lt;/p&gt;

&lt;h2&gt;
  
  
  Attribution is the minimum visible duty
&lt;/h2&gt;

&lt;p&gt;If your app publicly displays results derived from Open Food Facts, place a clear attribution where users encounter the data, and link to the source and licence. Do not hide it in a repository that mobile users never see.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Example:&lt;/strong&gt; “Food data from &lt;a href="https://world.openfoodfacts.org" rel="noopener noreferrer"&gt;Open Food Facts&lt;/a&gt;, available under the &lt;a href="https://opendatacommons.org/licenses/odbl/1-0/" rel="noopener noreferrer"&gt;ODbL&lt;/a&gt;.”&lt;/p&gt;

&lt;p&gt;Keep source metadata in your own objects so attribution can survive caching, exports and UI redesigns.&lt;/p&gt;

&lt;h2&gt;
  
  
  Produced work, collective database or derivative database?
&lt;/h2&gt;

&lt;p&gt;The licence distinguishes between outputs made from querying a database and a database based on or substantially transformed from the original. A chart, product page or calculated meal summary may be a produced work. A separate database alongside OFF may be collective. A normalized copy containing a substantial part of OFF may be derivative.&lt;/p&gt;

&lt;p&gt;Those categories affect notice and share-alike responsibilities. They are also fact-specific. Renaming columns does not necessarily make the underlying database independent, and repeatedly extracting small portions can still become substantial.&lt;/p&gt;

&lt;h2&gt;
  
  
  Network access is not identical to distributing a copy
&lt;/h2&gt;

&lt;p&gt;ODbL defines “convey” around enabling another person to receive a copy of the database or derivative database, and notes that mere interaction through a network is not itself conveying. That does not eliminate public-use attribution or every possible share-alike question; it simply means an API and a downloadable database are not automatically the same act.&lt;/p&gt;

&lt;h2&gt;
  
  
  Build compliance into the data model
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;Store &lt;code&gt;source&lt;/code&gt; and source record identifiers.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Keep a licence registry rather than one global licence string.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Render attribution near public data.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Track whether images have separate terms.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Document imports, transformations and exports.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Review bulk-download and customer-export features separately.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Do not promise customers that a commercial API subscription transfers ownership of upstream data.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Questions to answer before launch
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;Are we displaying individual results or distributing a substantial database?&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Do customers receive exports or only interact with an API?&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Did we combine OFF with proprietary records, and can provenance be separated?&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Are we redistributing product images?&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Where will users see attribution?&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Who owns modifications and who must receive corresponding database access?&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Start with the actual &lt;a href="https://opendatacommons.org/licenses/odbl/1-0/" rel="noopener noreferrer"&gt;ODbL 1.0 text&lt;/a&gt; and &lt;a href="https://openfoodfacts.github.io/documentation/docs/Product-Opener/api/tutorials/license-be-on-the-legal-side/" rel="noopener noreferrer"&gt;Open Food Facts reuse guidance&lt;/a&gt;. Contact Open Food Facts about its data when guidance is unclear, and seek legal advice for your particular distribution model.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Dietly position:&lt;/strong&gt; DietlyAPI is built primarily on Open Food Facts. Paid plans provide access to Dietly's hosted normalization, indexing, ranking and support; they do not cancel ODbL duties that apply upstream.&lt;/p&gt;

&lt;h2&gt;
  
  
  A commercial subscription and a data licence solve different problems
&lt;/h2&gt;

&lt;p&gt;A hosted provider can charge for infrastructure, normalization, support, service limits and contractual permission to use its API commercially. That commercial agreement can coexist with an open licence on upstream data. The invoice does not prove that the provider owns every underlying fact, and the open licence does not require the provider to operate servers for free.&lt;/p&gt;

&lt;p&gt;Review both layers: the provider's service terms and each upstream licence relevant to the returned data. They can impose different duties. Service terms may limit credentials, caching or throughput; ODbL addresses database rights, attribution and share-alike. Product images, brand assets and user-contributed text may introduce still other terms.&lt;/p&gt;

&lt;h2&gt;
  
  
  Attribution should survive every surface
&lt;/h2&gt;

&lt;p&gt;List all places where data appears: web pages, native apps, PDFs, exports, widgets, email reports and public API responses. Decide where the attribution notice belongs on each surface and how a user can reach the licence. A single footer on the marketing website may not inform someone viewing an embedded result inside a customer application.&lt;/p&gt;

&lt;p&gt;Automate attribution where possible. If the API returns source metadata, map it to a maintained notice rather than hard-coding a sentence in several clients. Include source and licence information in exports so downstream users do not accidentally strip provenance. Test attribution as part of release QA, just like privacy links and billing disclosures.&lt;/p&gt;

&lt;h2&gt;
  
  
  Think carefully before offering bulk exports
&lt;/h2&gt;

&lt;p&gt;An interactive application that shows individual query results differs operationally and legally from a feature that lets customers download millions of normalized records. Bulk export can cross from using produced works into conveying a database or derivative database. It also changes the security, privacy and infrastructure profile of the product.&lt;/p&gt;

&lt;p&gt;Before adding export, document which records are included, which sources they came from, what transformations were applied and which notices accompany the file. Decide how recipients can obtain any corresponding database that must be offered under applicable share-alike terms. This is the point where qualified advice is especially valuable.&lt;/p&gt;

&lt;h2&gt;
  
  
  Do not confuse facts with protected content
&lt;/h2&gt;

&lt;p&gt;A calorie number may be a factual value, while a product photograph, label artwork or long description can carry copyright or trademark concerns independent of database rights. Preserve separate image metadata and avoid assuming the database licence covers the image. Open Food Facts explicitly calls out its image licence separately.&lt;/p&gt;

&lt;p&gt;Logos and brand names may be necessary to identify products, but their presence does not imply sponsorship. Use them accurately, avoid misleading endorsement and review marketplace policies where an app store or advertising platform imposes additional rules.&lt;/p&gt;

&lt;h2&gt;
  
  
  Keep a small compliance record
&lt;/h2&gt;

&lt;p&gt;Maintain a short architecture note naming sources, licences, import paths, transformations, public surfaces, attribution text and owners. Update it when adding another dataset or export feature. This record helps engineers avoid accidental regressions and gives counsel a concrete system to review instead of an abstract question about “using food data.”&lt;/p&gt;

&lt;p&gt;Compliance is easier when provenance is designed in from the first table. Reconstructing it after records have been merged, cached and redistributed is expensive and sometimes impossible.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Originally published at &lt;a href="https://www.getdietly.com/developers/odbl-commercial-nutrition-apps/" rel="noopener noreferrer"&gt;getdietly.com&lt;/a&gt;. Data from the &lt;a href="https://www.getdietly.com/api" rel="noopener noreferrer"&gt;Dietly Nutrition API&lt;/a&gt; — 4.7M+ indexed foods, free tier available.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>opensource</category>
      <category>api</category>
      <category>data</category>
      <category>licensing</category>
    </item>
    <item>
      <title>Building Reliable Barcode Lookup for a Nutrition App</title>
      <dc:creator>Dietly</dc:creator>
      <pubDate>Sun, 19 Jul 2026 14:41:35 +0000</pubDate>
      <link>https://dev.to/dietly/building-reliable-barcode-lookup-for-a-nutrition-app-5926</link>
      <guid>https://dev.to/dietly/building-reliable-barcode-lookup-for-a-nutrition-app-5926</guid>
      <description>&lt;h1&gt;
  
  
  Building reliable barcode lookup for a nutrition app
&lt;/h1&gt;

&lt;p&gt;&lt;strong&gt;A successful scan is not the same thing as a successful product lookup.&lt;/strong&gt; The camera may decode a valid GTIN that your database has never seen, or multiple source rows may share the identifier. Treat “not found” and ambiguity as normal product states.&lt;/p&gt;

&lt;h2&gt;
  
  
  Keep identifiers as strings
&lt;/h2&gt;

&lt;p&gt;UPC, EAN and GTIN values are identifiers, not quantities. Store them as text so leading zeroes survive serialization and spreadsheet imports. GS1 notes that applications using a uniform 14-digit GTIN format add leading zeroes to shorter forms.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;function cleanBarcode(raw) {
  return raw.replace(/[^0-9]/g, "");
}

const code = cleanBarcode(scannerValue);
if (![8, 12, 13, 14].includes(code.length)) {
  showError("That does not look like a supported GTIN");
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Decide whether your API stores the scanned representation or canonical GTIN-14. If you normalize, do it during both ingestion and lookup, and preserve the original value for debugging.&lt;/p&gt;

&lt;h2&gt;
  
  
  Validate the check digit before calling the API
&lt;/h2&gt;

&lt;p&gt;A scanner library can still return truncated input or a manually entered typo. GTIN uses a modulo-10 check digit with alternating weights. Rejecting an invalid code saves a network request and gives a faster explanation.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;function validGtin(code) {
  if (!/^\d{8}$|^\d{12,14}$/.test(code)) return false;
  const digits = [...code].map(Number);
  const check = digits.pop();
  const sum = digits.reverse().reduce(
    (total, digit, i) =&amp;gt; total + digit * (i % 2 === 0 ? 3 : 1), 0);
  return (10 - (sum % 10)) % 10 === check;
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Use an exact index, never fuzzy barcode matching
&lt;/h2&gt;

&lt;p&gt;Names benefit from fuzzy search. Barcodes do not. A one-digit difference identifies another item or an invalid code. Put a B-tree index on the canonical barcode column and use equality.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;CREATE INDEX CONCURRENTLY foods_barcode_idx ON foods (barcode);

SELECT id, name, brand, confidence
FROM foods
WHERE barcode = $1;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Resolve duplicates deterministically
&lt;/h2&gt;

&lt;p&gt;Multiple source records can share a barcode because of imports, translations or packaging updates. Dietly prefers a row with an image, then higher internal confidence, then the lowest stable ID. Your exact signals may differ, but the order must be explicit and stable.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;ORDER BY
  (image_url IS NOT NULL) DESC,
  confidence DESC NULLS LAST,
  id ASC
LIMIT 1;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For high-stakes workflows, return several candidates or source metadata instead of silently selecting one. A calorie tracker can usually show one best record plus an “incorrect product?” action.&lt;/p&gt;

&lt;h2&gt;
  
  
  A 404 is a user journey
&lt;/h2&gt;

&lt;p&gt;When the API returns no match, offer text search, manual nutrition entry and, when your data source supports it, a contribution flow. Do not generate nutrition values merely because a barcode exists.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;const response = await fetch(`${API}/barcode/${code}`);
if (response.status === 404) {
  showNoMatch({ actions: ["Search by name", "Enter label manually"] });
  return;
}
if (!response.ok) throw new Error("Lookup temporarily unavailable");
showProduct(await response.json());
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Measure the right things
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;Scanner decode success&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Valid-GTIN rate&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Database hit rate by country and device locale&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Duplicate rate and winner changes&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;User corrections after a successful match&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Latency from scan to rendered product&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Do not market a universal barcode coverage percentage unless you have a representative, reproducible test set.&lt;/p&gt;

&lt;h2&gt;
  
  
  Normalize symbologies without erasing meaning
&lt;/h2&gt;

&lt;p&gt;A camera SDK may report UPC-A, EAN-13 or another symbology separately from the digits. Capture both. A 12-digit UPC can be represented as a 13-digit EAN with a leading zero, and GTIN systems may pad to 14 digits, but arbitrary padding is not a substitute for a documented canonicalization rule. Apply the same rule when importing suppliers, reading CSV files and serving API requests.&lt;/p&gt;

&lt;p&gt;Be careful with spreadsheet tooling. It may remove leading zeroes or display a long identifier in scientific notation. Validate imports before they reach the production table and keep rejected rows with reasons. Treating a barcode as an integer also makes JSON clients vulnerable to language-specific numeric limits. A string avoids all of these problems.&lt;/p&gt;

&lt;h2&gt;
  
  
  Separate scan errors from catalog misses
&lt;/h2&gt;

&lt;p&gt;Users need different messages for an unreadable image, an invalid check digit, a valid but unsupported code type, a valid GTIN absent from the catalog and a temporary server failure. Combining them into “product not found” sends people toward the wrong remedy. Let them rescan when decoding failed, correct digits when validation failed, search by name for a catalog miss and retry later for a network error.&lt;/p&gt;

&lt;p&gt;Mobile cameras often decode the same barcode repeatedly while the product remains in frame. Debounce identical scan events and cancel in-flight duplicates. Lock the result screen only after a valid response or deliberate no-match state. This avoids flicker and prevents one physical scan from consuming many API requests.&lt;/p&gt;

&lt;h2&gt;
  
  
  Cache hits and misses differently
&lt;/h2&gt;

&lt;p&gt;A successful product lookup can usually be cached because labels change less frequently than users revisit them. A miss should receive a shorter negative cache: the catalog may add that barcode in the next import or after a community contribution. Include the normalized identifier and relevant dataset edition in the cache key. Invalidate or version cached records when your response contract changes.&lt;/p&gt;

&lt;p&gt;For offline-first apps, store the product together with source and retrieval time. Make it clear when the result is cached, especially if users rely on recently changed allergens or formulations. The physical package remains the authoritative source for immediate safety decisions.&lt;/p&gt;

&lt;h2&gt;
  
  
  Duplicate handling needs observability
&lt;/h2&gt;

&lt;p&gt;Record how often an exact barcode returns multiple rows, which tie-breaker selected the winner and whether users reject that choice. A rising duplicate rate may indicate an ingestion regression. If the winner changes after a refresh, compare the old and new source, serving and nutrient values before assuming the higher confidence record is better.&lt;/p&gt;

&lt;p&gt;Do not deduplicate solely on a shared name or barcode without considering market and lifecycle information. Manufacturers sometimes reuse packaging conventions, databases contain historic records, and regional versions can differ. Deterministic ranking provides a safe default while preserving the underlying rows for review.&lt;/p&gt;

&lt;h2&gt;
  
  
  Test with a representative barcode set
&lt;/h2&gt;

&lt;p&gt;Build a dated test pack covering the countries and retailers your users actually scan. Include known hits, known misses, damaged labels, short GTINs, leading-zero cases and duplicated records. Publish the methodology if you make coverage claims. A random sample from your own database measures whether you can rediscover records you already possess; it does not measure real-world barcode coverage.&lt;/p&gt;

&lt;p&gt;Run the set through the full mobile path, not only the database query. Camera focus, lighting, orientation, network latency and result rendering all contribute to the experience. The lookup SQL can be instantaneous while the feature still feels unreliable.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Try it:&lt;/strong&gt; &lt;code&gt;GET https://api.getdietly.com/barcode/{code}&lt;/code&gt; performs indexed EAN/UPC lookup and returns one normalized food object or a normal 404 miss.&lt;/p&gt;

&lt;p&gt;Identifier reference: &lt;a href="https://www.gs1.org/standards/id-keys" rel="noopener noreferrer"&gt;GS1 identification keys&lt;/a&gt;.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Originally published at &lt;a href="https://www.getdietly.com/developers/reliable-barcode-lookup/" rel="noopener noreferrer"&gt;getdietly.com&lt;/a&gt;. Data from the &lt;a href="https://www.getdietly.com/api" rel="noopener noreferrer"&gt;Dietly Nutrition API&lt;/a&gt; — 4.7M+ indexed foods, free tier available.&lt;/em&gt;&lt;/p&gt;

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