Saltar al contenido

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

RecursoEndpointQué es
AlimentosGET /v1/foodsEl recurso principal: alimentos e ingredientes
Alimento por idGET /v1/foods/{id}Un alimento individual por su id de 12 caracteres
NutrientesGET /v1/nutrientsEl catálogo de nutrientes (p. ej. PROTEIN)
MarcasGET /v1/brandsCatálogo de marcas
Grupos de alimentosGET /v1/food-groupsCatá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 }
}
ClaveQué contiene
dataLas filas. Siempre un array en un endpoint de lista, aunque haya uno o ninguno
links.selfLa consulta que acabas de hacer, devuelta tal cual
links.nextLa URL de la página siguiente, o null cuando estás en la última
meta.has_moretrue mientras queden páginas: la misma señal que links.next con valor
meta.page_sizeCuá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:

GET
curl -G https://api.noms.sh/v1/foods \
  --data-urlencode "q=amonds" \
  -H "Authorization: Bearer $NOMS_KEY"
Response
200 OK
{
"data": [
{
"id": "TIgbNPnzCIjX",
"barcode": "00041570110645",
"name": "Roasted salted almonds",coincidió a pesar del error de tipeo 'amonds'
"description": null,
"scientific_name": null,
"country_of_origin": null,
"ingredients_text": "Almonds, sunflower oil, sea salt.",
"is_foundational": false,
"basis_unit": "grams",
"brand_id": "XoluRyOx9o3C",
"food_group_id": "NUTS_AND_SEEDS"
},
{
"id": "g8E3YqCcPnq6",
"barcode": "00000026359434",
"name": "Maple bourbon almonds",
"description": null,
"scientific_name": null,
"country_of_origin": null,
"ingredients_text": null,
"is_foundational": false,
"basis_unit": "grams",
"brand_id": null,null cuando el valor se desconoce; la clave nunca se omite
"food_group_id": null
}
],
"links": {
"self": "/v1/foods?q=amonds",
"next": null
},
"meta": {
"has_more": false,
"page_size": 10
}
}

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:

GET/v1/foods?barcode[eq]=00041570110645

Un campo=valor a secas significa eq, así que esta es la misma consulta:

GET/v1/foods?barcode=00041570110645

Repite el patrón para combinar filtros. Todos tienen que cumplirse a la vez:

GET/v1/foods?food_group_id=DAIRY&is_foundational=false

Por qué campos filtra cada endpoint

EndpointCamposOperadores
/v1/foodsid, barcode, name, brand_id, brand_name, food_group_id, country_of_origin, market_countrieseq, in
/v1/foodsis_foundationaleq
/v1/foodsnutrient[CODE]eq, gt, gte, lt, lte
/v1/brandsid, nameeq, in
/v1/food-groupsid, nameeq, in
/v1/nutrientsid, name, uniteq, 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:

GET/v1/foods?nutrient[PROTEIN][gte]=20&nutrient[TOTAL_SUGARS][lt]=5

Ordenar

Ordena con ?sort=. Un nombre de campo a secas ordena de forma ascendente; antepón - para descendente:

GET/v1/foods?sort=-name

Por qué campos ordena cada endpoint

EndpointClaves de ordenOrden por defecto
/v1/foodsid, name, brand_name, nutrient[CODE]id
/v1/brandsid, nameid
/v1/food-groupsid, nameid
/v1/nutrientsid, name, unitname

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:

GET/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:

GET/v1/foods/TIgbNPnzCIjX?include=brand,nutrients,serving_sizes

Un 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:

ClaveRecortaColumnas 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]brandid, name
fields[food-groups]food_groupid, name, icon_url
fields[nutrients]nutrientsid, name, unit, value
fields[serving_sizes]serving_sizesunit, quantity, grams, milliliters, descriptor, is_default
fields[images]imagestype, url
fields[market_countries]market_countriescountry_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:

GET
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"
Response
200 OK
{
"data": {
"id": "TIgbNPnzCIjX",
"name": "Roasted salted almonds",
"basis_unit": "grams",
"brand": {conservada por fields[foods], recortada a name por fields[brands]
"name": "Blue Diamond"
}
}
}

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.

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:

GET
curl https://api.noms.sh/v1/market \
  -H "Authorization: Bearer $NOMS_KEY"
Response
200 OK
{
"data": {
"chosen": null,tu mercado, o null hasta que elijas uno
"available": [los mercados donde hay alimentos disponibles
"AR",
"CL",
"MX",
"US"
],
"note": nullpor qué chosen es null en un plan de pago; null en el gratuito
}
}

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.

PUT
curl -X PUT https://api.noms.sh/v1/market \
  -H "Authorization: Bearer $NOMS_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "market": "CL"
  }'
Response
200 OK
{
"data": {
"chosen": "CL",
"available": [
"AR",
"CL",
"MX",
"US"
],
"note": null
}
}

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.

Próximos pasos