Consultas y modelo de datos
Esta página cubre cómo leer datos: qué recursos puedes consultar, en qué envoltorio vuelven y cómo combinar las cuatro palancas (buscar, filtrar, ordenar y modelar) para obtener exactamente lo que necesitas. Para la referencia completa por endpoint, consulta el explorador de la API.
Los recursos
| Recurso | Endpoint | Qué es |
|---|---|---|
| Alimentos | GET /v1/foods | El recurso principal: alimentos e ingredientes |
| Alimento por id | GET /v1/foods/{id} | Un alimento individual por su id de 12 caracteres |
| Nutrientes | GET /v1/nutrients | El catálogo de nutrientes (p. ej. PROTEIN) |
| Marcas | GET /v1/brands | Catálogo de marcas |
| Grupos de alimentos | GET /v1/food-groups | Catálogo de grupos de alimentos |
El envoltorio
Todas las respuestas vienen envueltas. Nunca devolvemos un array suelto ni un objeto suelto, así que un solo parser sirve para todos los endpoints.
Un recurso individual vuelve como { "data": { … } }: una sola clave, con el
registro adentro.
Una lista agrega dos hermanas:
{ "data": [ { "id": "TIgbNPnzCIjX", "name": "Roasted salted almonds", … } ], "links": { "self": "/v1/foods?q=almonds", "next": "/v1/foods?q=almonds&cursor=b3V0..." }, "meta": { "has_more": true, "page_size": 10 } }
| Clave | Qué contiene |
|---|---|
data | Las filas. Siempre un array en un endpoint de lista, aunque haya uno o ninguno |
links.self | La consulta que acabas de hacer, devuelta tal cual |
links.next | La URL de la página siguiente, o null cuando estás en la última |
meta.has_more | true mientras queden páginas: la misma señal que links.next con valor |
meta.page_size | Cuántas filas trae una página completa, para distinguir una página corta del final |
Sigue links.next en vez de armarla tú: ya lleva tu q, tus filtros, tu orden y
el cursor. Ver Paginar.
Cada alimento en data trae el mismo núcleo plano (id, barcode, name,
description, scientific_name, country_of_origin, ingredients_text,
is_foundational, basis_unit, brand_id, food_group_id) con null donde el
valor se desconoce, en lugar de omitir la clave. Los objetos relacionados son
opcionales mediante include=, como verás en
Modela la respuesta, y basis_unit es el primero que
conviene conocer: es contra qué está medido cada valor nutricional
(Datos nutricionales).
Buscar
Pasa ?q= para buscar en el catálogo. Los resultados vuelven ordenados por
relevancia y la búsqueda tolera errores de tipeo: las palabras casi-correctas
igual encuentran el alimento adecuado:
curl -G https://api.noms.sh/v1/foods \ --data-urlencode "q=amonds" \ -H "Authorization: Bearer $NOMS_KEY"
Filtrar
Filtra nombrando el campo y después el operador entre corchetes:
campo[op]=valor. El más común es la coincidencia exacta, p. ej. resolver un
código de barras:
/v1/foods?barcode[eq]=00041570110645Un campo=valor a secas significa eq, así que esta es la misma consulta:
/v1/foods?barcode=00041570110645Repite el patrón para combinar filtros. Todos tienen que cumplirse a la vez:
/v1/foods?food_group_id=DAIRY&is_foundational=falsePor qué campos filtra cada endpoint
| Endpoint | Campos | Operadores |
|---|---|---|
/v1/foods | id, barcode, name, brand_id, brand_name, food_group_id, country_of_origin, market_countries | eq, in |
/v1/foods | is_foundational | eq |
/v1/foods | nutrient[CODE] | eq, gt, gte, lt, lte |
/v1/brands | id, name | eq, in |
/v1/food-groups | id, name | eq, in |
/v1/nutrients | id, name, unit | eq, in |
in recibe una lista separada por comas, así que
?food_group_id[in]=DAIRY,NUTS_AND_SEEDS alcanza a cualquiera de los dos grupos.
Dos de esos campos leen a través de una relación en vez de una columna del
alimento: brand_name compara contra el nombre de la marca, y market_countries
prueba pertenencia, así que ?market_countries=CL conserva todos los alimentos
que se venden en Chile.
Filtrar por el valor de un nutriente
nutrient[CODE][op]=valor compara el valor por 100 de un nutriente, donde CODE
es cualquier id de GET /v1/nutrients. Repite el parámetro para acumular
condiciones, y usa dos sobre el mismo código para expresar un rango:
/v1/foods?nutrient[PROTEIN][gte]=20&nutrient[TOTAL_SUGARS][lt]=5Los códigos de barras son GTIN de 14 dígitos
Los códigos de barras se guardan y se devuelven como un GTIN de 14 dígitos,
rellenado con ceros a la izquierda. El filtro normaliza tu valor a esa forma
antes de comparar, así que cualquier largo del mismo código encuentra el mismo
alimento. 041570110645, 0041570110645 y 00041570110645 funcionan igual.
Pásale al filtro lo que te entregue el escáner.
Los filtros omiten los valores ausentes
Una comparación sólo alcanza a las filas que tienen valor. En un campo que puede
ser null, is_foundational[eq]=false devuelve los alimentos marcados
explícitamente como false, no aquellos en los que nunca se determinó.
Ordenar
Ordena con ?sort=. Un nombre de campo a secas ordena de forma ascendente;
antepón - para descendente:
/v1/foods?sort=-namePor qué campos ordena cada endpoint
| Endpoint | Claves de orden | Orden por defecto |
|---|---|---|
/v1/foods | id, name, brand_name, nutrient[CODE] | id |
/v1/brands | id, name | id |
/v1/food-groups | id, name | id |
/v1/nutrients | id, name, unit | name |
Une claves con comas para agregar desempates, aplicados de izquierda a derecha:
?sort=brand_name,name. Todo orden termina en id, lo nombres o no, así que el
orden siempre es total y el corte de una página nunca cae en medio de un empate.
Una clave que no esté en la tabla devuelve 400 /problems/invalid-sort, cuyo
cuerpo lista las que sí habrían funcionado.
nutrient[CODE] rankea los alimentos por su valor por 100 de ese nutriente, así
que «primero los de más proteína» es un solo parámetro:
/v1/foods?sort=-nutrient[PROTEIN]Un sort junto con q desempata, no manda. Con los dos, los resultados se
ordenan por qué tan bien coinciden, y sort sólo separa los que coinciden igual
de bien. Para ordenar estrictamente por una columna, no envíes q.
Ordenar por algo que al alimento le falta lo deja último, no afuera.
?sort=-nutrient[FIBER] devuelve todos los alimentos que coincidieron: los que no
tienen fibra registrada simplemente van después de los que sí. Lo mismo con
?sort=brand_name y los alimentos sin marca. Agregar un orden nunca achica la
cantidad de resultados.
Modela la respuesta
Dos parámetros deciden qué vuelve. include= agrega objetos relacionados enteros
y fields[...]= recorta cualquier recurso a las columnas que nombres. Juntos te
permiten pedir exactamente lo que muestras.
include
include= incorpora objetos relacionados a cada alimento. Combínalos con comas:
/v1/foods/TIgbNPnzCIjX?include=brand,nutrients,serving_sizesUn alimento expone seis relaciones: brand, food_group, nutrients,
serving_sizes, images y market_countries. Sin include=, un alimento sólo
trae su núcleo plano, con brand_id y food_group_id como los identificadores
que seguirías después.
fields[...]
fields[<recurso>]= son campos dispersos: una lista separada por comas de las
columnas que ese recurso debe devolver. La clave es el nombre del recurso en
inglés, no el campo que estás recortando, y cada recurso de la respuesta tiene su
propia clave:
| Clave | Recorta | Columnas que puedes nombrar |
|---|---|---|
fields[foods] | El alimento en sí | id, barcode, name, description, scientific_name, country_of_origin, ingredients_text, is_foundational, basis_unit, brand_id, food_group_id, más el nombre de cualquier relación |
fields[brands] | brand | id, name |
fields[food-groups] | food_group | id, name, icon_url |
fields[nutrients] | nutrients | id, name, unit, value |
fields[serving_sizes] | serving_sizes | unit, quantity, grams, milliliters, descriptor, is_default |
fields[images] | images | type, url |
fields[market_countries] | market_countries | country_code |
La clave de primer nivel también manda sobre las anidadas. Una relación
incorporada es, ella misma, un campo del alimento, así que una relación que pides
con include= pero dejas fuera de fields[foods] desaparece de la respuesta.
Nómbrala ahí para conservarla y después recorta sus columnas con su propia clave.
Abajo, brand sobrevive porque fields[foods] la lista, y food_group no:
curl -G https://api.noms.sh/v1/foods/TIgbNPnzCIjX \ --data-urlencode "include=brand,food_group" \ --data-urlencode "fields[foods]=id,name,basis_unit,brand" \ --data-urlencode "fields[brands]=name" \ -H "Authorization: Bearer $NOMS_KEY"
Nombrar una columna que el recurso no tiene devuelve
400 /problems/invalid-fields, y el cuerpo lista las que sí tiene.
Paginar
Las listas usan paginación por keyset (cursor), rápida y estable incluso en
conjuntos grandes. Una página trae 10 filas por defecto y 20 como máximo;
fíjalo con page_size y luego sigue links.next hasta que sea null:
let url = "https://api.noms.sh/v1/foods?q=oat&page_size=20"; const all = []; while (url) { const res = await fetch(url, { headers: { "X-API-Key": process.env.NOMS_KEY } }); const page = await res.json(); all.push(...page.data); url = page.links.next; // null en la última página }
Evita descargar una página que ya tienes
Las respuestas de catálogo traen un ETag: la huella de esa respuesta exacta.
Devuélvelo como If-None-Match y, si nada cambió, recibes 304 Not Modified sin
cuerpo en vez de la página completa.
const url = "https://api.noms.sh/v1/foods?q=oat"; const headers = { "X-API-Key": process.env.NOMS_KEY }; const first = await fetch(url, { headers }); const etag = first.headers.get("ETag"); // mas tarde, para la misma URL const again = await fetch(url, { headers: { ...headers, "If-None-Match": etag } }); if (again.status === 304) { // no cambio nada - conserva lo que ya parseaste }
El tag cubre todo lo que determina la respuesta: tu plan y tu mercado, include,
fields, sort y el cursor de la página. Un tag sirve solo para el request que lo
generó. Si lo reutilizas en otro, recibes el 200 completo. Nunca la página
equivocada.
Tu propio cliente también puede guardar la respuesta 300 segundos
(Cache-Control: private, max-age=300). Dentro de esa ventana un request repetido ni
siquiera sale de tu proceso. La revalidación es lo que viene después.
En el plan gratuito Taster, /v1/foods no es cacheable y no trae tag: sus
resultados dependen del mercado que elegiste y esa elección puede cambiar. Los
catálogos de nutrientes, marcas y grupos de alimentos sí lo son en todos los planes.
Un 304 también consume tu cuota
El request llegó a la API, así que se mide y se limita igual que un 200, y los
headers RateLimit-* vuelven con él. Lo que ahorras al revalidar es ancho de banda
y tiempo, no requests. Revisa Límites y cuotas.
Elegir tu mercado
El plan gratuito Taster sirve un mercado: los alimentos de un solo país. La
elección pertenece a tu cuenta, no a una clave. Todas tus claves sirven el mismo
mercado, y cambiarlo las mueve todas a la vez. Elígelo en tu panel, o consulta tu
selección actual y los mercados disponibles con GET /v1/market:
curl https://api.noms.sh/v1/market \ -H "Authorization: Bearer $NOMS_KEY"
Defínelo (o cámbialo) con PUT /v1/market. Hasta que elijas, las peticiones a foods
devuelven 403 /problems/market-not-selected; una vez definido, foods queda acotado
a ese mercado.
curl -X PUT https://api.noms.sh/v1/market \ -H "Authorization: Bearer $NOMS_KEY" \ -H "Content-Type: application/json" \ -d '{ "market": "CL" }'
Elegir un mercado sin alimentos disponibles devuelve 422 /problems/unknown-market
(incluye available); los planes de pago sirven todos los mercados, así que definir
uno devuelve 409 /problems/market-not-applicable, y GET responde chosen: null
con un note que explica que tu plan ya los cubre todos. Para acotar una consulta
puntual, añádele un filtro market_countries. Igual que GET /v1/usage, ambas
llamadas son no medidas.
El plan gratuito prioriza la búsqueda
Elegir un mercado es uno de los tres límites del plan gratuito Taster; los otros dos afectan cómo listas. Límites y cuotas los cubre los tres en un solo lugar.
Próximos pasos
- Escenarios comunes: recetas listas para copiar que combinan estas cuatro palancas.