TL;DR: Map each item in the array to a fetch call and wrap them around Promise.all().
Promise.all(
this.cartProducts.map(p =>
fetch(`/api/products/getDetails/${p.clientId}`).then(res => res.json())
)
).then(products => console.log(products));
Explanation
fetch() returns a Promise a.k.a "I promise I'll give you a value in the future". When the time comes, that future value is resolved and passed to the callback in .then(callback) for further processing. The result of .then() is also a Promise which means they're chainable.
// returns Promise
fetch('...')
// also returns Promise
fetch('...').then(res => res.json())
// also returns Promise where the next resolved value is undefined
fetch('...').then(res => res.json()).then(() => undefined)
So the code below will return another Promise where the resolved value is a parsed Javascript object.
fetch('...').then(res => res.json())
Array.map() maps each item to the result of the callback and return a new array after all callbacks are executed, in this case an array of Promise.
const promises = this.cartProducts.map(p =>
fetch("...").then(res => res.json())
);
After calling map, you will have a list of Promise waiting to be resolved. They do not contains the actual product value fetched from the server as explained above. But you don't want promises, you want to get an array of the final resolved values.
That's where Promise.all() comes into play. They are the promise version of the Array.map() where they 'map' each Promise to the resolved value using the Promise API.
Because of that Promise.all() will resolve yet another new Promise after all of the individual promises in the promises array have been resolved.
// assuming the resolved value is Product
// promises is Promise[]
const promises = this.cartProducts.map(p =>
fetch("...").then(res => res.json())
);
Promise.all(promises).then(products => {
// products is Product[]. The actual value we need
console.log(products)
})