0

I'm writing a server route that makes api calls.

I need to make two different fetch requests cause I need more info that's coming in the first fetch.

The problem is that I'm declaring a variable out of the promise scope and for some reason, my res.send is not awaiting until the array gets full.

I need to iterate until result 9 (I can't use theDogApi's predefined filters to show nine results!)

if (req.query.name) {
    var myRes = [];
    fetch(`https://api.thedogapi.com/v1/breeds/search?name=${req.query.name}&apikey=${key}`)
        .then(r => r.json())
        .then( data => {

            for (let i = 0; i < 8 && i < data.length; i++) {
                fetch(`https://api.thedogapi.com/v1/images/${data[i].reference_image_id
                    }`)
                    .then(r => r.json())
                    .then(datos => {

                        myRes.push({ ...data[i], ...datos });
                    })
            }
        })
        .then(res.send(myRes))
}

I'll appreciate the help!

ggorlen
  • 44,755
  • 7
  • 76
  • 106
florci1
  • 71
  • 1
  • 4
  • 1
    Use PromiseAll. Similar to [this](https://stackoverflow.com/a/60710890/15237039) – Wei Yan Feb 18 '21 at 22:53
  • 1
    Good time to use an `async function` with `await`, or some other Promise structure. – StackSlave Feb 18 '21 at 22:53
  • 1
    You also don't have a return statement in your second `.then()` and your third `.then()` will produce a syntax error. – Randy Casburn Feb 18 '21 at 22:54
  • @WeiYan that's helpfull but i don't really know how to implement it on my code.. – florci1 Feb 18 '21 at 23:01
  • Reopening because this question is not really about returning a value from the function. it's more about managing loops of asynchronous operations and other asynchronous coding mistakes. – jfriend00 Feb 18 '21 at 23:39
  • Many things wrong here, Here's one part that is wrong: `.then(res.send(myRes))` is never correct., Change to `.then(() => { res.send(myRes);})`. You have to pass a function reference to `.then()` so it can be called LATER when the promise actually resolved. – jfriend00 Feb 18 '21 at 23:41

3 Answers3

0

Here is an example of an async function unsing await:

async function fun(queryName, key){
  const a = [], p, j = [];
  let firstWait = await fetch(`https://api.thedogapi.com/v1/breeds/search?name=${req.query.name}&apikey=${key}`);
  let firstJson = await firstWait.json(); // must be an Array
  for(let i=0,n=8,j,l=firstJson.length; i<n && i<l; i++){
    a.push(fetch('https://api.thedogapi.com/v1/images/'+firstJson[i].reference_image_id));
  }
  p = await Promise.all(a);
  for(let v of p){
    j.push(v.json());
  }
  return Promise.all(j);
}
// assumes req, req.query, req.query.name, and key are already defined
fun(req.query.name, key).then(a=>{
  // a is your JSON Array
});
StackSlave
  • 10,613
  • 2
  • 18
  • 35
  • 1
    that `for` loop is so hard on the eyes. have you seen `for..of`, or `Array.prototype.map`? – Mulan Feb 19 '21 at 00:34
  • @Thankyou, of course I know about those loops. My second loop is a `for..of` loop. The `for` loop is very simple to read. `;` separates `initial_vars; condition; increment_vars`. `,` separates `initial_vars`, and could separate `increment_vars`, as well. OP needed the loop to be less that `8`. Yeah, I could have got rid of `, j = []` up top, and done like `const j = p.map(o=>o.json());` instead of the `for..of` loop, but I doubt it would increase performance, and that's easy to read. – StackSlave Feb 19 '21 at 01:28
0

You can try using Promise.all to turn your array of fetch calls into an aggregate promise that resolves to an array of responses when all have arrived. If any fail, the whole thing fails (use Promise.allSettled if you don't want all-or-nothing semantics). Don't forget to catch the error.

Although the code doesn't show it, be sure to check response.ok to make sure the request actually succeeded before calling .json(). Throwing an error if !repsonse.ok and handling it in the .catch block is a typical strategy. Writing a wrapper on fetch is not a bad idea to avoid verbosity.

Lastly, note that Array#slice replaces the for loop. For arrays with fewer than 8 elements, it'll slice as many as are available without issue.

// mock everything
const fetch = (() => {
  const responses = [
    {
      json: async () => 
        [...Array(10)].map((e, i) => ({reference_image_id: i}))
    },
    ...Array(10)
      .fill()
      .map((_, i) => ({json: async () => i})),
  ];
  return async () => responses.shift();
})();
const req = {query: {name: "doberman"}};
const key = "foobar";
const res = {send: response => console.log(`sent ${response}`)};
// end mocks

fetch(`https://api.thedogapi.com/v1/breeds/search?name=${req.query.name}&apikey=${key}`)
  .then(response => response.json())
  .then(data => 
    Promise.all(data.slice(0, 8).map(e =>
      fetch(`https://api.thedogapi.com/v1/images/${e.reference_image_id}`)
        .then(response => response.json())
    ))
  )
  .then(results => res.send(results))
  .catch(err => console.error(err))
;
ggorlen
  • 44,755
  • 7
  • 76
  • 106
0

JSON

Here's my hot take: Stop using low-level functions like fetch every time you want to get JSON. This tangles up fetching logic every time we want to get a bit of JSON. Write getJSON once and use it wherever you need JSON -

const getJSON = s =>
  fetch(s).then(r => r.json())

const data =
  await getJSON("https://path/to/some/data.json")

// ...

URL and URLSearchParams

Another hot take: Stop writing all of your URLs by hand. This tangles URL writing/rewriting with all of your api access logic. We can setup a DogApi endpoint once, with a base url and an apikey -

const DogApi =
  withApi("https://api.thedogapi.com/v1", {apikey: "0xdeadbeef"})

And now whenever we need to touch that endpoint, the base url and default params can be inserted for us -

const breed = 
  // https://api.thedogapi.com/v1/breeds/search?apikey=0xdeadbeef&name=chihuahua
  await getJSON(DogApi("/breeds/search", {name}))
    

// ...

withApi has a simple implementation -

const withApi = (base, defaults) => (pathname, params) =>
{ const u = new URL(url) // <- if you don't know it, learn URL
  u.pathname = pathname 
  setParams(u, defaults)
  setParams(u, params)
  return u.toString()
}

function setParams (url, params = {})
{ for (const [k,v] of Object.entries(params))
    url.searchParams.set(k, v)  // <- if you don't know it, learn URLSearchParams
  return url
}

fruits of your labor

Now it's dead simple to write functions like imagesForBreed, and any other functions that touch JSON or your DogApi -

async function imagesForBreed (name = "")
{ if (name == "")
    return []

  const breed = 
    await getJSON(DogApi("/breeds/search", {name}))

  const images =
    data.map(v => getJSON(DogAPI(`/images/${v.reference_image_id}`))
  
  return Promise.all(images)
}

And your entire Express handler is reduced to a single line, with no need to touch .then or other laborious API configuration -

async function fooHandler (req, res)
{ 
   res.send(imagesForBreed(req.query.name))
}    
Mulan
  • 129,518
  • 31
  • 228
  • 259