'Optimise multiple api requests with Promise.all()

I'm trying to optimise multiple made to an 3rd party API (Spotify) in my NestJS API. My code is working but I do not find a way to optimise it.

 @Get('')
 async getAllData(@Req() req: Request) {
    const token = req.cookies.access_token;
    const user = this.userService.getCurrentUserProfile(token);

    const periods = ['short_term', 'medium_term', 'long_term'];
    const types = ['artists', 'tracks'];

    const getUserTopData = async () => {
      const promise = [];
      types.forEach((type) => {
        periods.forEach(async (period) => {
          promise.push(
            this.personalizationService.getUserTopArtistsTracks(
              type as 'artists' | 'tracks',
              {
                time_range: period as
                  | 'short_term'
                  | 'medium_term'
                  | 'long_term',
                limit: 1,
              },
              token,
            ),
          );
        });
      });
      return promise;
    };

    const promises = await getUserTopData();
    const [
      artistsShortTerm,
      artistsMediumTerm,
      artistsLongTerm,
      tracksShortTerm,
      tracksMediumTerm,
      tracksLongTerm,
    ] = promises;

    return Promise.all([
      user,
      artistsShortTerm,
      artistsMediumTerm,
      artistsLongTerm,
      tracksShortTerm,
      tracksMediumTerm,
      tracksLongTerm,
    ])
      .then((values) => {
        const user = values[0];
        const artistsShortTerm = values[1];
        const artistsMediumTerm = values[2];
        const artistsLongTerm = values[3];
        const tracksShortTerm = values[4];
        const tracksMediumTerm = values[5];
        const tracksLongTerm = values[6];

        return {
          user,
          artistsShortTerm,
          artistsMediumTerm,
          artistsLongTerm,
          tracksShortTerm,
          tracksMediumTerm,
          tracksLongTerm,
        };
      })
      .catch((err) => {
        return err;
      });
  }

this.personalizationService.getUserTopArtistsTracks() function which executes the request :

export class PersonalizationService {
  constructor(private httpService: HttpService) {}

  async getUserTopArtistsTracks(
    type: 'artists' | 'tracks',
    query: {
      time_range: 'short_term' | 'medium_term' | 'long_term';
      limit: number;
      offset?: number;
    },
    token: string,
  ): Promise<SpotifyApi.UsersTopArtistsResponse> {
    const topArtists = this.httpService
      .get<SpotifyApi.UsersTopArtistsResponse>(
        `https://api.spotify.com/v1/me/top/${type}?${qs.stringify(query)}`,
        {
          headers: { Authorization: `Bearer ${token}` },
        },
      )
      .toPromise();

    return (await topArtists).data;
  }
}

A call to my controller on NestJS return a JSON like that :

enter image description here

I think my code is too long and that there is a more shorter way to do it. But I don't see how.



Solution 1:[1]

const periods = ['short_term', 'medium_term', 'long_term'];
const types = ['artists', 'tracks'];

// line up promises in an array
const getUserTopData = () =>
  types.flatMap((type) =>
    periods.map((period) =>
      this.personalizationService.getUserTopArtistsTracks(/* ... */)
    )
  )
);

// wait for promises (result order will reflect array order)
const results = await Promise.all(getUserTopData());

// give the result list back its structure
return types.reduce((obj, type) => {
  obj[type] = periods.reduce((obj, period) => {
    obj[period] = results.shift();
    return obj;
  }, {});
  return obj;
}, {});

This will produce a data structure like this:

{
  short_term: {artists: '...', tracks: '...'},
  medium_term: {artists: '...', tracks: '...'},
  long_term: {artists: '...', tracks: '...'}
}

Feel free to unpack the results array differently.

Solution 2:[2]

No need to create a promise function if you don't need it, you can return a promise for an async function. Don't use async keyword for a sync function.

@Get('')
 async getAllData(@Req() req: Request) {
    const token = req.cookies.access_token;

    const periods = ['short_term', 'medium_term', 'long_term'];
    const types = ['artists', 'tracks'];

    const getUserTopData = () => {
      const promise = [];
      types.forEach((type) => {
        periods.forEach((period) => {
          promise.push(
            this.personalizationService.getUserTopArtistsTracks(
              type as 'artists' | 'tracks',
              {
                time_range: period as
                  | 'short_term'
                  | 'medium_term'
                  | 'long_term',
                limit: 1,
              },
              token,
            ),
          );
        });
      });
      return promise;
    };

    const [
      user,
      artistsShortTerm,
      artistsMediumTerm,
      artistsLongTerm,
      tracksShortTerm,
      tracksMediumTerm,
      tracksLongTerm,
    ] = await Promise.all([
      this.userService.getCurrentUserProfile(token),
      ...getUserTopData()
    ]);

    return {
      user,
      artistsShortTerm,
      artistsMediumTerm,
      artistsLongTerm,
      tracksShortTerm,
      tracksMediumTerm,
      tracksLongTerm,
    };
  }

Solution 3:[3]

When you use Promise.all, you are already doing asynchronous requests in parallel. Unless spotify let's you get more data with less API requests, you will not find a better solution since you need to query Spotify.

However, the code clarity/quality can be improved:

  • The controllers shouldn't handle the logic

So redirect your controller to a service >

@Get("")
async getAllData(@Req() req: Request) {
   const token = req.cookies.access_token;
   // even that can be within the function
   const user = this.userService.getCurrentUserProfile(token);

   return await this.userService.getUserTopData(user, token);
}
  • Then within your service, define the type directly in your constants.

    async getUserTopData(user: User, token: string) {
      try {
        const periods: ("short_term" | "medium_term" | "long_term")[] = ["short_term", "medium_term", "long_term"];
        const types: ("artists" | "tracks")[] = ["artists", "tracks"];
    
        const promises = [];
         types.forEach(type =>
          periods.forEach(period =>
            promises.push(
              this.personalizationService.getUserTopArtistsTracks(type, { timeRange: period, limit: 1 }, token)
            ),
          ),
        );
    
        promises.push(this.getCurrentUserProfile(token));
    
        const [
          user,
          artistsShortTerm,
          artistsMediumTerm,
          artistsLongTerm,
          tracksShortTerm,
          tracksMediumTerm,
          tracksLongTerm,
        ] = await Promise.all(promises);
    
        return {
          user,
          artistsShortTerm,
          artistsMediumTerm,
          artistsLongTerm,
          tracksShortTerm,
          tracksMediumTerm,
          tracksLongTerm,
        };
      } catch (e) {
        // Do something here
      }
    }
    
  • Handle potentials errors (try, catch, exception handlers,...)

Sources

This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.

Source: Stack Overflow

Solution Source
Solution 1
Solution 2 hoangdv
Solution 3 callmemath