Over a million developers have joined DZone.
{{announcement.body}}
{{announcement.title}}

Real-World Angular Series, Part 3a: Fetching and Displaying API Data

DZone's Guide to

Real-World Angular Series, Part 3a: Fetching and Displaying API Data

Learn how to fetch data from Angular and your API in order to populate your application, and how to create a loading icon for UX/UI concerns.

· Web Dev Zone ·
Free Resource

Learn how error monitoring with Sentry closes the gap between the product team and your customers. With Sentry, you can focus on what you do best: building and scaling software that makes your users’ lives better.

The second part of this tutorial (Part 2a and Part 2b) covered authentication, authorization, feature planning, and data modeling.

The third installment in the series covers fetching data from MongoDB with a Node API and displaying and filtering the data with Angular.

API: Fetching Events

Let's pick up right where we left off last time. We have data in our database, so it's time to retrieve it with the API. We'll start by writing four endpoints that will get data from MongoDB:

  • List of public events starting in the future.
  • List of all public and private events (admin access required).
  • Event details (authentication required).
  • List of RSVPs associated with an event (authentication required).

Open up the server api.js file and let's begin.

GET Future Public Events

We'll start with an /api/events endpoint that retrieves all public events with a start date in the future from the events collection.

Recall that we already created and required our Event and Rsvpmongoose schema in Part 2: Data Modeling. We can now use those schemas to execute MongoDB collection methods with mongoose.

Add the following code to the API Routes section of the api.js file:

// server/api.js
...
/*
 |--------------------------------------
 | API Routes
 |--------------------------------------
 */

 const _eventListProjection = 'title startDatetime endDatetime viewPublic';

  // GET list of public events starting in the future
  app.get('/api/events', (req, res) => {
    Event.find({viewPublic: true, startDatetime: { $gte: new Date() }},
      _eventListProjection, (err, events) => {
        let eventsArr = [];
        if (err) {
          return res.status(500).send({message: err.message});
        }
        if (events) {
          events.forEach(event => {
            eventsArr.push(event);
          });
        }
        res.send(eventsArr);
      }
    );
  });

  ...

This endpoint does not require any authentication. We'll pass the find() query as {viewPublic: true, startDatetime: { $gte: new Date() } because we only want public events with a starting datetime greater than or equal to now.

We also want to pass a projection (see the first example). Projections state which fields we want to be returned in the documents that match our query. If no projection is specified, all fields are returned. In our case, we don't need descriptions or locations in main event listings, so our projection will contain only the properties we do want to be returned.

In the callback, we'll handle errors and iterate over any results, pushing them to an array that will be returned. We want an empty array if there are no events since a lack of event documents simply means none have been created yet. Pretty straightforward!

GET All Public and Private Events

Next, we'll create a similar endpoint that will return all events: /api/events/admin. This time, we want authentication and admin privileges before we'll send any data. We can implement this like so:

// server/api.js
  ...
  // GET list of all events, public and private (admin only)
  app.get('/api/events/admin', jwtCheck, adminCheck, (req, res) => {
    Event.find({}, _eventListProjection, (err, events) => {
        let eventsArr = [];
        if (err) {
          return res.status(500).send({message: err.message});
        }
        if (events) {
          events.forEach(event => {
            eventsArr.push(event);
          });
        }
        res.send(eventsArr);
      }
    );
  });

  ...

The code for this endpoint is very similar to the route fetching public events, but we'll include both the jwtCheck and adminCheck middleware. We won't add any parameters to the query object because we want to retrieve all events in the database. We'll pass the same _eventListProjection to leave out locations and descriptions.

Note: It's worthwhile to note that this endpoint is simply for admin display purposes. We want the admin to be able to see and interact with a listing of public and private events. However, authenticated users can still see private event details too, they just need to know the direct link and can't access them from a list.

GET Event Details

Now, we'll fetch an event by ID with an /api/event/:id endpoint:

// server/api.js
  ...
  // GET event by event ID
  app.get('/api/event/:id', jwtCheck, (req, res) => {
    Event.findById(req.params.id, (err, event) => {
      if (err) {
        return res.status(500).send({message: err.message});
      }
      if (!event) {
        return res.status(400).send({message: 'Event not found.'});
      }
      res.send(event);
    });
  });

  ...

This authorized endpoint needs to have a parameter passed to it when called. The parameter should be the event ID so we can use the findById() method. If no event is found matching the ID we passed, we'll send a bad request error. Otherwise, we'll return the event.

GET RSVPs for an Event

Finally, we'll retrieve a list of all the RSVPs for a specific event: /api/event/:eventId/rsvps. RSVPs in our app are transparent to all authenticated users; many people want to know if their friends are attending the same events they are.

// server/api.js
  ...
  // GET RSVPs by event ID
  app.get('/api/event/:eventId/rsvps', jwtCheck, (req, res) => {
    Rsvp.find({eventId: req.params.eventId}, (err, rsvps) => {
      let rsvpsArr = [];
      if (err) { 
        return res.status(500).send({message: err.message});
      }
      if (rsvps) {
        rsvps.forEach(rsvp => {
          rsvpsArr.push(rsvp);
        });
      }
      res.send(rsvpsArr);
    });
  });

  ...

We'll find RSVPs by matching the eventId, which will be passed with the request as a parameter. We want to return an array whether or not there are RSVPs since a lack of RSVPs does not indicate an error.

Angular: Fetching Events

Now that we have API routes for fetching events, we need to access these routes in our Angular app so we can display events data.

Create an API Service

To do this, we'll create an API service. Let's generate the service now:

$ ng g service core/api

This command creates a file called api.service.ts in the src/app/corefolder. Open the service and add the following code:

// src/app/core/api.service.ts
import { Injectable } from '@angular/core';
import { Http, Response } from '@angular/http';
import { AuthHttp } from 'angular2-jwt';
import { AuthService } from './../auth/auth.service';
import { Observable } from 'rxjs/Rx';
import 'rxjs/add/operator/map';
import 'rxjs/add/operator/catch';
import { ENV } from './env.config';
import { EventModel } from './models/event.model';
import { RsvpModel } from './models/rsvp.model';

@Injectable()
export class ApiService {

  constructor(
    private http: Http,
    private authHttp: AuthHttp,
    private auth: AuthService) { }

  // GET list of public, future events
  getEvents$(): Observable<EventModel[]> {
    return this.http
      .get(`${ENV.BASE_API}events`)
      .map(this._handleSuccess)
      .catch(this._handleError);
  }

  // GET all events - private and public (admin only)
  getAdminEvents$(): Observable<EventModel[]> {
    return this.authHttp
      .get(`${ENV.BASE_API}events/admin`)
      .map(this._handleSuccess)
      .catch(this._handleError);
  }

  // GET an event by ID (login required)
  getEventById$(id: string): Observable<EventModel> {
    return this.authHttp
      .get(`${ENV.BASE_API}event/${id}`)
      .map(this._handleSuccess)
      .catch(this._handleError);
  }

  // GET RSVPs by event ID (login required)
  getRsvpsByEventId$(eventId: string): Observable<RsvpModel[]> {
    return this.authHttp
      .get(`${ENV.BASE_API}event/${eventId}/rsvps`)
      .map(this._handleSuccess)
      .catch(this._handleError);
  }

  private _handleSuccess(res: Response) {
    return res.json();
  }

  private _handleError(err: Response | any) {
    const errorMsg = err.message || 'Error: Unable to complete request.';
    if (err.message && err.message.indexOf('No JWT present') > -1) {
      this.auth.login();
    }
    return Observable.throw(errorMsg);
  }

}

We'll need to make unauthenticated and authenticated requests, so we'll import both Http and AuthHttp. We also need AuthService to prompt login if no JWT is found when attempting to make an authenticated request. We'll create streams with our API calls, so we'll import Observable, as well as the map and catch operators from RxJS. We need ENV from our environment config to get the appropriate API URIs. Finally, in order to declare the types for our event streams, we need the models (EventModel and RsvpModel) we created earlier.

Once Http, AuthHttp, and AuthService are added to the constructor, we can use the HTTP methods to create observables of API data. We expect to receive streams of type EventModel[] (an array of events) for our two event list endpoints, a single EventModel when retrieving event details by ID, and RsvpModel[] when retrieving all RSVPs for an event.

Because we're taking advantage of angular2-jwt, all we need to do to make authenticated requests is use authHttp instead of http. The authHttpmethod attaches the necessary Authorization header using the access token stored in local storage from the authentication service we created in Angular: Basic Authentication in Part 2.

If we need to pass parameters to the request, such as with the getEventById$(id) and getRsvpsByEventId$(eventId) methods, we'll specify the arguments when calling the endpoint from our components.

Finally, we'll handle successes and errors. A successful API call returns the response as JSON while a failed call checks the error message and prompts a fresh login if necessary, canceling the observable and producing an error if something else went wrong.

Provide API Service in App Module

We want our API service to be available throughout our app, so let's provide it in our app.module.ts:

// src/app/app.module.ts
...
import { ApiService } from './core/api.service';
...
@NgModule({
  ...
  providers: [
    ...,
    ApiService
  ],
  ...
})
...

We can now import the service in any of our components to use its methods.

Create a Loading Component

Since we'll be making asynchronous API calls, it's ideal to also have a loading state. Alternatively, we could use route resolve to prevent routes from loading until the necessary API data has been returned, but this can give an app the appearance of sluggishness while navigating. Instead, we'll show a loading icon with a very simple component.

Generate the loading component like so:

$ ng g component core/loading --is --it --flat

We want this to be a single-file component, so we'll set a few options with the Angular CLI:

  • --is: alias for --inline-styles
  • --it: alias for --inline-template
  • --flat: do not generate a containing directory

Now let's grab a suitable loading image. You easily can make your own at loading.io, or you can download this one:

svg loading icon

We'll create an images directory inside our src/assets folder and place the loading icon there.

Then we'll open our loading.component.ts and add the markup and a few simple styles:

// src/app/core/loading.component.ts
import { Component } from '@angular/core';

@Component({
  selector: 'app-loading',
  template: `
    <img src="/assets/images/loading.svg">
  `,
  styles: [`
    :host {
      display: block;
    }
    img {
      display: block;
      margin: 20px auto;
      width: 50px;
    }
  `]
})
export class LoadingComponent {
}

We can remove the OnInit functionality and the constructor function. The template is very simple: just the host element with an image. We can style the host element using the special selector :host (the host element is the component's custom element, <app-loading> in this case).

Add a Loading Component to the Callback Component

Let's replace the Loading... text in our callback component with our new loading component. Open the callback.component.html file and replace its contents with the following:

<!-- src/app/pages/callback/callback.component.html -->
<app-loading></app-loading>

Now we'll see the spinner after the login redirect instead of plain text. We'll also use the loading component when making API calls across other components.

Tune in tomorrow to learn how to create a utility service, create a filter/sort service, and make a home component event list.

What’s the best way to boost the efficiency of your product team and ship with confidence? Check out this ebook to learn how Sentry's real-time error monitoring helps developers stay in their workflow to fix bugs before the user even knows there’s a problem.

Topics:
web dev ,angular ,web application development

Published at DZone with permission of

Opinions expressed by DZone contributors are their own.

{{ parent.title || parent.header.title}}

{{ parent.tldr }}

{{ parent.urlSource.name }}