How to query nested data from database using multiple API calls in Angular?

Viewed 225

I am learning angular and API service. Currently, I use router to redirect to the group detail page using group id

Here is what I want to achieve: In the detail page, I would like to display the user names and user cities in the angular page, which requires multiple API calls (maybe need in order) to get data from the group data.

  1. I need to use route id/group id to get the group object via getGroup()
  2. Each group contains multiple users Id
  3. (to get user names) I need to iterate each user Ids to get user object which contains user names and city Ids via getUser()
  4. (to get city names) I need to iterate each city Ids to get city object which contains city ids and city names via getCity()

The overview of all the datas:

######### group data
{
  "groups": [
    {
      "id": "group_id1",
      "users": ["user_id1", "user_id2"]
    },
    {
      "id": "group_id2",
      "users": ["user_id3", "user_id4"]
    }
   ]
}

######### user data
{
  "users": [
    {
      "id": "user_id1",
      "city": ["city_id1", "city_id2"]
    },
    {
      "id": "user_id2",
      "city": ["city_id3", "city_id4"]
    },
    {
      "id": "user_id3",
      "city": ["city_id1", "city_id3"]
    },
    {
      "id": "user_id4",
      "city": ["city_id2", "city_id4"]
    }
   ]
}

######### city data
{
  "cities": [
    {
      "id": "city_id1",
      "name": "NY"
    }
    {
      "id": "city_id2",
      "name": "BOS"
    }
    {
      "id": "city_id3",
      "name": "SF"
    }
    {
      "id": "city_id4",
      "name": "LA"
    }
   ]
}

For now, I am able to get the group , but I don't how to get the user names and the cities.

## component.ts
export class displayComponent implements OnInit {
  group$!: Observable<group>;

  constructor(
    private router: Router,
    private route: ActivatedRoute,
    private groupService: GroupService,
    private userService: UserService,
    private cityService: CityService,
  ) {}

  ngOnInit(): void {
    this.group$ = this.route.params.pipe(
      switchMap((params) => this.groupService.getGroup(params.id))
    );

// how to iterate all **user Id** to get user names via `getUser(**user Id**)`
// how to iterate all **city Id** to get city names via `getCity(**city Id**)`
  }

## html
<div *ngIf="group$ | async as group">
  <p> {{ group.id }} </p>
 <!-- user names placeholder-->
 <!-- user city names placeholder-->
</div>
3 Answers

I wouldn't recommend you to make a request for each user and city individually. That operation would take forever since you would be making many http request for one page rendering. You can fetch all cities, and all users from the database and filter locally instead. Or you can make the API return all needed data on a single request. But if neither of these options are available for you, you can chain the switchMap pipe operation. For example:

switchMap(group => {
  let observable = of({});

  group.users.forEach(userId => {
    observable = observable.pipe(
      switchMap(() => {
        return this.userService.getUser(userId);
      }),
      tap(user => {
        ...
      })
    );
  });

  return observable;
})

EDIT: This code shows how to make this by fetch all cities and users first.

## component.ts
export class displayComponent implements OnInit {
  userMap: { [userId: string]: User };
  cityMap: { [cityId: string]: City };
  group: Group;

  constructor(
    private router: Router,
    private route: ActivatedRoute,
    private groupService: GroupService,
    private userService: UserService,
    private cityService: CityService,
  ) {}

  ngOnInit(): void {
    this.userMap = {};
    this.cityMap = {};

    this.userService.getAllUsers().pipe(
      switchMap(users => {
        users.forEach(user => {
          this.userMap[user.id] = user;
        })
        return this.cityService.getAllCities();
      }),
      switchMap(cities => {
        cities.forEach(city => {
          this.cityMap[city.id] = city;
        });

        return this.route.params;
      }),
      switchMap(params => {
        return this.groupService.getGroup(params.id);
      })
    ).subscribe(group => {
      this.group = group;
    });
  }

## html
<div *ngIf="group">
  <p> {{ group.id }} </p>
  <ul>
    <li *ngFor="let userId of group.users">
      {{ userMap[userId].name }}<br>
      <div class="city" *ngFor="let cityId of userMap[userId].city">
       {{ cityMap[cityId].name }}
      </div>
    </li>
  </ul>
</div>

Chaining the requests would do the trick, it would go something like this:

Your_Source.pipe(
      switchMap((params) => this.groupService.getGroup(params.id))
      switchMap((params) => this.userService.getUser(params.id))
      switchMap((params) => this.cityService.getCity(params.id))
    );

Hope this helped.

It is about RxJs. Should be something like that. Not tested though

private getCitiesInfo() {
 this.http.get('getGroupByIdApi')
  .pipe(
    map(group => groups.users),
    switchMap(userIds => this.constructGetUsersRequest(userIds)),
    map(usersData => usersData.flat().map(user => user.city)),
    switchMap(cityIds => this.constructGetCityRequests(cityIds))
  ).subscribe();
}


private constructGetUsersRequest(userIds: string[]): Observable<User>[] {
    const requests: Observable<User>[] = userIds.map(userId => this.http.get('userIdGetApi'));
    return combineLatest(requests);
}

private constructGetCityRequests(cityIds: string[]): Observable<City>[] {
    const requests: Observable<City>[]  = cityIds.map(cityId => this.http.get('cityIdGetApi'));
    return combineLatest(requests);
}
Related