Aller au contenu

L'application

Contrat d'API de la base de données alimentaires (expérimental)

Expérimental. Les requêtes et réponses qu'une base de données alimentaires doit fournir, pour que FOOD_DB_API_URL puisse pointer vers un serveur autre que lowcarbcheck.org

Cette page est traduite automatiquement à partir de la documentation en anglais.

Expérimental. Cette page décrit ce que le serveur applicatif demande aujourd'hui à une base de données alimentaires. Ce n'est pas une API stable. Une version mineure peut la modifier, et le journal des modifications l'indiquera. Le seul serveur qui l'implémente est lowcarbcheck.org. Aucun serveur open source n'existe encore, et il n'y a pas de test de conformité. Si tu en crées un, dis-le-nous dans un ticket, pour que le contrat puisse évoluer avec un deuxième utilisateur.

openplate recherche les aliments par leur nom via son propre serveur. Par défaut, ce serveur interroge lowcarbcheck.org. FOOD_DB_API_URL le fait pointer vers un autre serveur qui répond aux mêmes requêtes. configuration.md explique la clé et le quota. Cette page détaille les requêtes et les réponses.

Contenu du contrat

Le serveur applicatif effectue quatre types de requêtes. Seule la première est obligatoire.

RequêteUtilisationSi ton serveur ne l'a pas
POST /api/v1/foods/searchRecherche d'aliments, et vitamines et minéraux d'une nouvelle entréeLa recherche d'aliments est vide. Les analyses conservent les chiffres de l'IA.
GET /api/v1/foods/{slug}Vitamines pour les anciennes entrées du journal (le rattrapage)Les anciennes entrées restent sans vitamines.
GET /api/v1/nutrientsApports de référence, pour que l'application affiche les déficitsL'application montre ce qu'une journée contient, sans les déficits.
GET /api/v1/nutrients/{slug}/foods« Aliments riches en ce nutriment » sur l'écran du nutrimentLa liste est vide.

Le serveur applicatif traite chaque échec comme une réponse vide. Un point de terminaison manquant, un délai dépassé, un statut non-2xx ou un corps non conforme ne bloque jamais une analyse ou une page du journal. C'est le principe du fail-open.

Règles pour chaque requête

  • L'adresse de base correspond à FOOD_DB_API_URL. Les chemins ci-dessus y sont ajoutés.
  • JSON uniquement. Les requêtes envoient accept: application/json. Les réponses sont en JSON UTF-8.
  • La clé. Quand FOOD_DB_API_KEY est défini, chaque requête porte authorization: Bearer <key>. Ton serveur décide de la signification d'une clé. Sans clé définie, l'en-tête est absent.
  • Le délai d'expiration est de 4 secondes par requête. Une réponse plus lente compte comme un échec.
  • Les champs inconnus sont ignorés. Tu peux ajouter des champs librement. Un champ optionnel manquant signifie « aucun chiffre ».
  • Pour 100 g. Tous les chiffres nutritionnels sont donnés pour 100 g d'aliment. L'énergie est en kcal. Les macronutriments sont en grammes.
  • null n'est pas 0. null signifie « cette source n'a aucun chiffre ». 0 signifie « mesuré, et la valeur est zéro ». L'application ne compte que les chiffres mesurés, donc n'envoie jamais 0 pour une valeur inconnue.
  • Vie privée. La requête de recherche transporte le nom de l'aliment dans un corps POST, pas dans l'adresse, afin qu'aucun nom ne se retrouve dans un journal d'accès. Conserve cette propriété : ne journalise pas les corps de requête. L'application n'envoie aucun compte, aucun journal et aucune donnée corporelle. Voir architecture.md.

POST /api/v1/foods/search

Corps de la requête :

json
{ "q": "hähnchenbrust", "locale": "de", "limit": 10 }
  • q est le nom de l'aliment que la personne a saisi ou que l'IA a identifié.
  • locale est l'un des éléments suivants : en, de, fr, it, es, tr. Renvoie les titres dans cette langue quand tu en disposes.
  • limit vaut 10 aujourd'hui.

Réponse :

json
{
  "results": [
    {
      "slug": "haehnchenbrust-roh",
      "locale": "de",
      "title": "Hähnchenbrust, roh",
      "canonicalName": "Chicken breast, raw",
      "url": null,
      "imageUrl": null,
      "macrosPer100g": {
        "kcal": 106, "protein": 23.1, "fat": 1.2,
        "carbs": 0, "fiber": 0, "sugars": 0, "polyols": 0
      },
      "netCarbsPer100g": 0,
      "attribution": "BLS 4.0, CC BY 4.0",
      "score": 0.92,
      "origin": "bls",
      "portionSize": null,
      "vitamins": { "vitaminA": 9, "vitaminC": 0, "vitaminB12": 0.3 },
      "minerals": { "sodium": 60, "potassium": 370 }
    }
  ]
}
ChampTypeSignification
slugstring, requisUn identifiant stable pour l'aliment. L'application le stocke et le redemande plus tard dans la requête de détail.
localestring, requisLa langue de title.
titlestring, requisLe nom à afficher.
canonicalNamestring, requisUn nom en anglais, utilisé comme libellé stable.
urlstring ou nullUne page pour l'aliment. null lorsqu'aucune n'existe. Une absence vaut null.
imageUrlstring ou null, requisUne image, ou null.
macrosPer100gobject, requiskcal, protein, fat, carbs, fiber, sugars, polyols. Chacun est un nombre ou null. Les sept clés doivent être présentes.
netCarbsPer100gnumber ou null, requisGlucides sans les fibres. Voir Glucides.
attributionstring ou nullLa mention de licence pour les données importées. L'application l'affiche.
scorenombre, obligatoirePertinence, de 0 à 1. Voir Classement.
originstring ou nullLa provenance de l'aliment. Elle détermine la façon dont l'application lit carbs. Voir Glucides.
portionSizenombre ou nullUne portion type en grammes.
vitaminsobjet ou nullVoir Vitamines et minéraux. Omets-le quand l'aliment n'a aucune donnée de vitamines.
mineralsobjet ou nullLa même chose, pour les minéraux.

Glucides

Deux sources affichent les glucides de deux manières différentes, et l'application doit savoir laquelle un aliment utilise. Le champ origin l'indique :

originBaseCe que macrosPer100g.carbs signifie
bls, curateddisponible (UE)Glucides sans les fibres. Les fibres forment une ligne distincte.
fdc, user, proposaltotal (États-Unis)Glucides avec les fibres. Les glucides nets correspondent aux glucides moins les fibres.
absent ou toute autre valeurinconnuL'application le traite comme un total, et la personne peut le modifier manuellement.

Si tes données sont au format UE, envoie origin: "curated". Si elles sont au format américain, envoie origin: "fdc". Une mauvaise valeur ici fausse les valeurs de glucides et de calories de chaque aliment concerné, vérifie donc avec un aliment que tu connais.

netCarbsPer100g est ta propre valeur de glucides nets pour 100 g, et l'application lui fait confiance plutôt qu'à son propre calcul. null signifie « inconnu pour cet aliment ». L'application calcule alors les glucides nets à partir des macronutriments et de la base ci-dessus. N'envoie jamais 0 pour une valeur inconnue.

Classement

Trie results par pertinence, le meilleur en premier. L'application conserve ton ordre. Elle ignore les correspondances faibles selon score :

  • Un score de 0,4 ou plus doit correspondre à une vraie correspondance textuelle (exacte, préfixe, mot ou sous-chaîne). L'application l'ignore en dessous de 0,45.
  • Un score inférieur à 0,4 doit correspondre uniquement à une faute de frappe. L'application l'ignore en dessous de 0,3.
  • Une correspondance par faute de frappe ne doit jamais atteindre ou dépasser 0,4, sinon elle passe devant une vraie correspondance.

Vitamines et minéraux

Chaque objet d'aliment contient deux blocs facultatifs, vitamins et minerals. Un bloc comporte ces clés et aucune autre que l'application ne lise :

BlocClésUnité
vitaminsbetaCarotene, vitaminA, vitaminD, vitaminB9, vitaminB12µg
vitaminsvitaminC, vitaminE, vitaminB1, vitaminB2, vitaminB6mg
mineralsnacl, potassium, sodium, calcium, magnesium, zinc, phosphorus, ironmg

Chaque valeur est un nombre ou null. Une clé omise vaut null. Le bloc lui-même a trois états distincts :

  1. Bloc absent (ou null) : cet aliment n'a aucune donnée de vitamines ou de minéraux.
  2. La clé vaut null : le bloc existe et cette valeur précise est inconnue.
  3. La clé est un nombre : une valeur mesurée. 0 est une vraie donnée.

L'application considère qu'un nutriment est couvert pour une journée seulement si une part suffisante du poids des aliments comporte une valeur mesurée (60 % aujourd'hui). De faux zéros donnent l'impression que la personne n'a consommé aucune part d'une vitamine, envoie donc null si tu n'es pas sûr.

La vitamine A est exprimée en équivalents d'activité rétinol (RAE), qui comprend déjà les caroténoïdes. betaCarotene sert de contexte, ce n'est pas un ajout. Si ta source fournit des équivalents rétinol (RE) ou des unités internationales, convertis avant d'envoyer, sinon les totaux seront faux.

Détail de l'aliment

GET /api/v1/foods/{slug}?locale=de

L'application demande les vitamines d'un aliment consigné avant que la fonction des vitamines n'existe. Elle envoie le slug issu d'un ancien résultat de recherche.

  • Trouvé : statut 200 et un objet d'aliment avec slug, vitamins et minerals. Les mêmes blocs que pour la recherche. Les autres champs sont ignorés.
  • Introuvable dans cette région : statut 404 avec exactement ce corps : { "error": "not_found" }. L'application teste ensuite les autres régions (de, en, fr, it, es, avec la langue de la personne en priorité), de sorte qu'un aliment connu dans une seule région reste trouvé. Tout autre corps 404 est considéré comme un échec, pas comme « introuvable ».
  • Trouvé, et aucun bloc : 200 avec les deux blocs absents. L'application enregistre « vérifié, sans données » et ne redemande plus.

Chaque recherche peut te coûter un crédit, jusqu'à cinq pour un slug qu'aucune région ne connaît. L'application interroge une fois par aliment distinct et par appareil.

Apports de référence

GET /api/v1/nutrients

Les valeurs ci-dessous ne servent que d'exemple de structure. Ce ne sont pas de vrais apports de référence.

json
{
  "nutrients": [
    {
      "slug": "vitamin-c",
      "unit": "mg",
      "kind": "target",
      "rdaDe": {
        "source": "DGE 2024",
        "male":   { "14-18": 105, "19-30": 110, "31-50": 110, "51-70": 110, "over_70": 110 },
        "female": { "14-18": 90,  "19-30": 95,  "31-50": 95,  "51-70": 95,  "over_70": 95 },
        "pregnancy": 105,
        "lactation": 125
      },
      "rdaEu": null,
      "rdaUs": null
    }
  ]
}
  • slug doit être l'un des slugs ci-dessous. L'application ignore les autres.
  • kind vaut target (« en as-tu assez pris ? ») ou ceiling (« es-tu resté en dessous ? »). Aujourd'hui, seul le sodium est un ceiling. Une valeur inconnue ou manquante est interprétée comme target.
  • rdaDe, rdaEu et rdaUs sont trois publications distinctes : la DGE, l'EFSA et les États-Unis (NASEM). Envoie celles que tu as et null pour les autres. L'application affiche une seule référence à la fois, selon le réglage de l'instance, sans jamais en mélanger deux.
  • Les tranches d'âge sont exactement 14-18, 19-30, 31-50, 51-70 et over_70. Les cinq sont requises pour les deux sexes. Il n'y a pas de tranche plus jeune. L'application n'affiche aucune référence pour une personne de moins de 14 ans.
  • pregnancy et lactation sont des nombres optionnels. L'application les utilise uniquement en local. Rien sur une personne ne t'est jamais envoyé.

Slugs de nutriments :

Clé de l'applicationSlug
vitaminAvitamin-a-retinol
vitaminCvitamin-c
vitaminDvitamin-d
vitaminEvitamin-e-tocopherole
vitaminB1vitamin-b-1-thiamine
vitaminB2vitamin-b-2-riboflavin
vitaminB6vitamin-b-6-pyridoxine
vitaminB9folic-acid-folate
vitaminB12vitamin-b-12-cobalamin
potassiumpotassium
sodiumsodium
calciumcalcium
magnesiummagnesium
zinczinc
phosphorusphosphorus
ironiron

betaCarotene n'a pas de référence propre, car il fait partie de la vitamine A.

Aliments riches en un nutriment

GET /api/v1/nutrients/{slug}/foods?locale=en&limit=8

json
{
  "nutrient": "vitamin-c",
  "unit": "mg",
  "foods": [
    { "slug": "acerola-raw", "title": "Acerola, raw", "url": null, "imageUrl": null,
      "netCarbsPer100g": 7.7, "attribution": null, "value": 1677.6 }
  ]
}

Trie foods du plus riche au moins riche. value est la quantité pour 100 g et n'est jamais null. Omets les aliments sans valeur plutôt que de les envoyer avec 0.

Erreurs

StatutSignificationCe que fait l'application
401La clé est incorrecte ou manquanteEnregistre invalid_key, affiche un message, continue de fonctionner avec les chiffres de l'IA
429Dépassement du quota ou de la limite de requêtesEnregistre rate_limited, ou allowance_exhausted si le corps l'indique
autre choseÉchecTraité comme "aucune réponse", sans avertissement

Pour 401 et 429, tu peux envoyer un corps sous la forme { "error": { "code": "allowance_exhausted" } }. Les codes reconnus sont invalid_key, rate_limited et allowance_exhausted. Un code inconnu se rabat sur la raison sous-entendue par le statut. L'application ne met jamais un refus en cache.

Teste ton serveur

Exécute ces requêtes sur ton serveur. Chacune doit renvoyer du JSON conforme aux sections ci-dessus.

bash
BASE=https://food.example.org
curl -s -X POST "$BASE/api/v1/foods/search" -H 'content-type: application/json' \
  -d '{"q":"apple","locale":"en","limit":3}'
curl -s "$BASE/api/v1/foods/apple-raw?locale=en"
curl -s "$BASE/api/v1/foods/does-not-exist?locale=en" -w '\n%{http_code}\n'   # 404, {"error":"not_found"}
curl -s "$BASE/api/v1/nutrients"

Définis ensuite FOOD_DB_API_URL sur le serveur de l'application, redémarre-le et recherche un aliment dans l'application. Un aliment consigné avec un bloc vitamins compte alors dans la carte des vitamines et minéraux de sa journée du journal. Si ce n'est pas le cas, consulte le journal du serveur de l'application : un refus est consigné au niveau warning avec les 16 premiers caractères de la clé et jamais la clé entière.

Ce qui n'est pas couvert

  • Aucun serveur n'est fourni. Tu en crées un, ou tu utilises lowcarbcheck.org.
  • Propositions d'aliments (POST /api/v1/foods/proposals, FOOD_DB_BACKFILL) sont une fonctionnalité distincte, propre à LowCarbCheck, et ne font pas partie de ce contrat.
  • Aucun versionnement pour le moment. Les chemins portent /v1, mais la structure peut encore changer tant que cette page indique "expérimental".
  • Les schémas réseau résident dans le code. La source normative actuelle est app/services/food-resolution/schema.ts (recherche et détail), app/lib/nutrient-reference.ts (apports de référence et aliments riches) et app/services/food-db/micronutrients-wire.ts (la route de rétroremplissage propre à l'application, qui ne fait pas partie de ce contrat).

Modifier cette page sur GitHub