Lesson Monday

In this lesson, we'll build a simple application that pulls information from one of NASA's Open APIs. We’ll start off by reviewing the basics of making API calls. Then we’ll cover how to make an API call in Angular and how to display the response data to the user.

Registering for an API Key

First, we need to register for an API key by visiting this page. After entering your information, you should be redirected immediately to a page that lists your API key. Not all API keys are returned so promptly; sometimes it will take an organization days to respond. It's important to consider how long it will take to get an API key when planning our projects.

Experimenting with API Calls

Next, let's try out an API request in Postman. If you did not read this lesson introducing Postman, read through it now. Here you can enter your API key, build sample requests with multiple parameters, and see the JSON data they return.

We'll experiment with the Mars Rover Photos API. Following the example listed in the documentation, let's construct an API call and test it in Postman. Make sure to replace DEMO_KEY with your own API key. Remember, you can click the Params button and add api_key as a key and your NASA API key as the value to keep things organized.

https://api.nasa.gov/mars-photos/api/v1/rovers/curiosity/photos?earth_date=2017-01-01&camera=mast&api_key=DEMO_KEY

When we click Send, a GET request is made to the URL we provide, and JSON-formatted data is returned in the response body:

{
  "photos": [
    {
      "id": 605121,
      "sol": 1566,
      "camera": {
        "id": 22,
        "name": "MAST",
        "rover_id": 5,
        "full_name": "Mast Camera"
      },
      "img_src": "http://mars.jpl.nasa.gov/msl-raw-images/msss/01566/mcam/1566MR0079890140800214E01_DXXX.jpg",
      "earth_date": "2017-01-01",
      "rover": {
        "id": 5,
        "name": "Curiosity",
        "landing_date": "2012-08-06",
        "launch_date": "2011-11-26",
        "status": "active",
        "max_sol": 1737,
        "max_date": "2017-06-25",
        "total_photos": 316990,
        "cameras": [
          {
            "name": "FHAZ",
            "full_name": "Front Hazard Avoidance Camera"
          },
          {
            "name": "NAVCAM",
            "full_name": "Navigation Camera"
          },
          {
            "name": "MAST",
            "full_name": "Mast Camera"
          },
          {
            "name": "CHEMCAM",
            "full_name": "Chemistry and Camera Complex"
          },
          {
            "name": "MAHLI",
            "full_name": "Mars Hand Lens Imager"
          },
          {
            "name": "MARDI",
            "full_name": "Mars Descent Imager"
          },
          {
            "name": "RHAZ",
            "full_name": "Rear Hazard Avoidance Camera"
          }
        ]
      }
    },
    {
      "id": 605122,
      "sol": 1566,
      "camera": {
        "id": 22,
        "name": "MAST",
        "rover_id": 5,
        "full_name": "Mast Camera"
      },
      "img_src": "http://mars.jpl.nasa.gov/msl-raw-images/msss/01566/mcam/1566MR0079890130800213D01_DXXX.jpg",
      "earth_date": "2017-01-01",
      "rover": {
        "id": 5,
        "name": "Curiosity",
        "landing_date": "2012-08-06",
        "launch_date": "2011-11-26",
        "status": "active",
        "max_sol": 1737,
        "max_date": "2017-06-25",
        "total_photos": 316990,
        "cameras": [
          {
            "name": "FHAZ",
            "full_name": "Front Hazard Avoidance Camera"
          },
          {
            "name": "NAVCAM",
            "full_name": "Navigation Camera"
          },
          {
            "name": "MAST",
            "full_name": "Mast Camera"
          },
          {
            "name": "CHEMCAM",
            "full_name": "Chemistry and Camera Complex"
          },
          {
            "name": "MAHLI",
            "full_name": "Mars Hand Lens Imager"
          },
          {
            "name": "MARDI",
            "full_name": "Mars Descent Imager"
          },
          {
            "name": "RHAZ",
            "full_name": "Rear Hazard Avoidance Camera"
          }
        ]
      }
    },
...

Note: To view JSON in Chrome nicely, add the JSONView extension, or use a site like JSON Pretty Print.

In addition to our api_key query parameter, we also added the earth_date and camera parameters. This narrows our search to a particular camera and a particular day. As you can see, there’s still more information than just an image URL in the response body. We will address how to access this data shortly.

Implementing API Data in Angular

Let's create an Angular CLI app to work with the data we receive from this API call:

$ ng new mars-rover

Make sure to add the appropriate setup code to app.component.html (Add <router-outlet></router-outlet>) and to create app.routing.ts as we’ve done for our other projects.

Using a Service with API Data

Next, we need to create a service to contain our API call. We want all of our API calls in services so that the data they retrieve is accessible throughout our projects. Each API we use should have a separate service to keep things organized.

$ ng g service mars-rover-api-photos

This creates two files. As before, we will ignore the test file. First, let's make sure the service file has all the imports we will use.

src/app/mars-rover-api-photos.service.ts
import { Injectable } from '@angular/core';
import { Http, Response } from '@angular/http';
import { Observable } from 'rxjs/Observable';

@Injectable()
export class MarsRoverApiPhotos {
  constructor() { }
}
...

As covered in the lessons on dependency injection and services, the Injectable import allows us to use the decorator @Injectable, which makes our service usable by our other files.

The Http and Response imports gives us access to Angular's built in http service. In order to use this service, it must be included in our list of imports it into our app.module.ts file. Luckily, Angular CLI does this by default.

app.module.ts
import { HttpModule } from '@angular/http';
…

@NgModule({
  declarations: [
    …
  ],
  imports: [
    HttpModule,
    …
  ],
  …
})
…

The Observable import lets us treat the data returned from API calls as Observables. If you did not read the optional lesson on Observables, go back and read it now.

Now, we'll add a parameter to our constructor:

src/app/mars-rover-api-photos.service.ts
...
export class MarsRoverApiPhotos {
  constructor(private http: Http) { }
}
...

This declares that our service uses Angular's Http service and will refer to it as http. We have declared that we want this property to be private. We want this to be the only class that has access to this particular instance of the Http service. This helps keep our code compartmentalized.

We need to add a method that utilizes the http service to make an API call. This method will simply return the results of the API call. We'll plan to let users choose which date and which camera they would like to view the photos from, so we'll declare it with two parameters.

src/app/mars-rover-api-photos.service.ts
...
getByDateAndCamera(date: string, camera: string) {
  return this.http.get("https://api.nasa.gov/mars-photos/api/v1/rovers/curiosity/photos?earth_date=" +date+ "&camera=" +camera+ "&api_key=DEMO_KEY")
}
...

We call the get() method on our http object. This method is part of Angular's http service. Other methods in the http service include post(), patch(), and delete(). The method signature for get() is get(url: string, options?: RequestOptionsArgs): Observable<Response>;, indicating that this method takes a url as its first argument, can optionally take a second argument (which we will not worry about), and returns an Observable.

Gathering User Data

Now that we have a service that houses our API call, let's create a component that will use our service. Don’t forget to that the component has been added to your list of imports at the top of app.module.ts, and to the declarations array listed under @NgModule. We also need to create a new path in app.routing.ts:

$ ng g component rover-form

This component will contain our form and logic for gathering values for the camera and sol parameters. Make sure to import this component and add a new route entry in for navigating to it inapp.routing.ts:

src/app/app.routing.ts
import { RoverFormComponent } from './rover-form/rover-form.component';
....
{
  path: '',
  component: RoverFormComponent
}

Here’s what our form should look like:

src/app/rover-form.component/rover-form.component.html
<h1>Curiosity Images</h1>

<form>
  <div class="form-group">
    <label for="camera">Date</label>
    <input type=date #roverDate>
  </div>
  <div class="form-group">
    <label for="camera">Camera</label>
    <select #roverCamera>
      <option value="FHAZ">Front Hazard Avoidance Camera</option>
      <option  value="RHAZ">Rear Hazard Avoidance Camera</option>
      <option value="MAST">Mast Camera</option>
      <option  value="CHEMCAM">Chemistry and Camera Complex</option>
      <option  value="MAHLI">Mars Hand Lens Imager</option>
    </select>
  </div>
  <button (click)="getRoverImages(roverDate.value, roverCamera.value)">Get Photos</button>
</form>

Now, we need to add the getRoverImages() method to our rover-form component. This method will collect the date and camera selection from the form and pass it to our service file. It will also set the value for this.photos to the response we get from our API. This response comes back as an Observable, much like the FirebaseObservables we have worked with in the past. Since Observables are asynchronous, we subscribe to the data return by getByDateAndCamera, then we call .json() on this data to cast the response object as a Javascript object. This allows us to render our results in the browser without using an async pipe. We use the fat arrow so this remains scoped to our class So we can update its photos property.

src/app/rover-form.component/rover-form.component.ts
import { Component } from '@angular/core';
import { Observable } from 'rxjs/Observable';
import { MarsRoverApiPhotos } from '../mars-rover-api-photos.service';

@Component({
  selector: 'app-rover-form',
  templateUrl: './rover-form.component.html',
  styleUrls: [ './rover-form.component.css' ],
  providers: [ MarsRoverApiPhotos ]
})

export class RoverFormComponent {
  photos: any[]=null;
  constructor(private marsRoverPhotos: MarsRoverApiPhotos) { }
  getRoverImages(date: string, camera: string) {
    this.marsRoverPhotos.getByDateAndCamera(date, camera).subscribe(response => {
        this.photos = response.json();
    });
  }
}

Notice that we declare an instance of service, marsRoverPhotos, in our constructor in the same way we did for our other services.

Displaying Data From an API Call

We still need a spot for displaying the rover photos. Let's create another component for that. After that, we'll create a model for our Photo object.

$ ng g component photos-list

Instead of routing to this component after the user submits the form, photos-list will be a child component of rover-form. This means we need to update our rover-form.component.html. Add the following code to the bottom of this file.

src/app/rover-form.component/rover-form.component.html
...
<div *ngIf="photos">
  <app-photos-list [childPhotos]="photos"></app-photos-list>
</div>
...

We have declared a property binding between childPhotos, a property we'll declare in our child component, and photos, the parent component property that our array of photos. This way, PhotosListComponent will have access to the photos returned by the API call.

Now, let's add code to display the results of our API call. Note also that we are using an ngIf directive, so that the component only renders after the form is submitted.

Let's make sure to capture our data from our parent component by using the @Input Decorator in our child component.

src/app/photos-list.component/photos-list.component.ts
import { Component, Input } from '@angular/core';

@Component({
  selector: 'app-photos-list',
  templateUrl: './photos-list.component.html',
  styleUrls: [ './photos-list.component.css' ],
  providers: [ ]
})

export class PhotosListComponent {
  @Input() childPhotos;
  constructor() { }
  ...
src/app/photos-list/photos-list.html
<h1>Martian Rover Photos:</h1>
<h2> From {{childPhotos.photos[0].earth_date}} by the {{childPhotos.photos[0].camera.name}}</h2>
<div *ngFor="let photo of childPhotos.photos;" class="photo">
  <img src = {{photo.img_src}}/>
</div>

Great! When we click Get Photos our photos-list component will render.

Styling our Images

Wow, our pages looks pretty messy without any formatting on our images. Let’s add some basic styling to photos-list.component.css before building out any more features.

> bower install bootstrap --save
src/app/photos-list.component/photos-list.component.css
img {
  height: 500px;
  width: 500px;
  padding: 1em;
  border: 5px solid brown;
}

.photo {
  object-fit: cover;
}

Much better!

Managing Unruly API Data

It’s very uncommon for the data returned by an API call to be in perfect condition. If you play around with this API, you’ll notice that not all cameras take pictures everyday. Let’s make our project more user-friendly by adding a message to display to users when no pictures were found for their search query. This also prevents our app from logging a nasty error when it tries to render the PhotosListComponent for a query that does not return any photos:

>> ERROR TypeError: Cannot read property 'earth_date' of undefined
    at Object.eval [as updateRenderer] (PhotosListComponent.html:2)
    at Object.debugUpdateRenderer [as updateRenderer] (core.es5.js:13144)
    at checkAndUpdateView (core.es5.js:12293)
    at callViewAction (core.es5.js:12651)
    at execComponentViewsAction (core.es5.js:12583)
    at checkAndUpdateView (core.es5.js:12294)
    at callViewAction (core.es5.js:12651)
    at execEmbeddedViewsAction (core.es5.js:12609)
    at checkAndUpdateView (core.es5.js:12289)
    at callViewAction (core.es5.js:12651)

>> ERROR CONTEXT DebugContext_ {view: Object, nodeIndex: 4, nodeDef: Object, elDef: Object, elView: Object}

First, we’ll create a property of type boolean in our RoverFormComponent class called noPhotos. We need to add logic to the getRoverImages() method to check whether the response contains at least one photo. If it does not, photos will be set to true.

src/app/rover-form.component/rover-form.component.ts
...
export class RoverFormComponent {
  photos: any[]=null;
  noPhotos: boolean=false;
  constructor(private marsRoverPhotos: MarsRoverApiPhotos) { }

  getRoverImages(date: string, camera: string) {
    this.noPhotos = false;
    this.marsRoverPhotos.getByDateAndCamera(date, camera).subscribe(response =>{
      if(response.json().photos.length > 0)
      {
        this.photos = response.json();
      }
      else {
        this.noPhotos = true;
      }
    });
  }
}

After calling .json() on our response object, we access its photos property, and then the length property of photos to check to make sure that at least one image was returned.

We’ll add the message to our html, and only show it if noPhotos is false.

src/app/rover-form.component/rover-form.component.html
…
<div *ngIf="photos && !noPhotos">
  <app-photos-list [childPhotos]="photos"></app-photos-list>
</div>
<div *ngIf="noPhotos">
    <h3>There were no photos taken on {{roverDate.value}} by the {{roverCamera.value}} camera.</h3>
</div>
...

Note that we have also modified the attributes of the first div. Now the if statement will only execute when photos is not null and when noPhotos is false.

Storing API keys

There is still one last thing to fix: we need to remove the reference to DEMO_KEY and use the unique API key that we requested from NASA Open APIs. We do not want other developers using our key. We’ve already seen how to go about hiding our keys in an Angular app when we configured our app for Firebase. We created a file called api-keys.ts in the app folder. We will store all of our API keys in this file. We must also make sure to add it to our .gitignore file. Add this file with the following code:

src/app/api-keys.ts
export const marsRoverKey = "{YOUR_KEY_HERE}"

We'll need to import the key into our service and alter the url string. Our updated service should look like:

src/app/mars-rover-api-photos.service.ts
import { marsRoverKey } from './api-keys';
import { Injectable } from '@angular/core';
import { Http, Response } from '@angular/http';
import { Observable } from 'rxjs/Observable';

@Injectable()
export class MarsRoverAPIPhotos {
  constructor(private http: Http) { }

  getByDateAndCamera(date: string, camera: string) {
    return this.http.get("https://api.nasa.gov/mars-photos/api/v1/rovers/curiosity/photos?earth_date=" +date+ "&camera=" +camera+ "&api_key="+marsRoverKey);
  }
}

Now, when we submit the form in our rover-form component, the component will grab the query parameter values from the form, pass these values to the proper method in our service, then render the child component with the results.

API Standards

As a side note, know that all APIs are different. Some use JSON in their response bodies, others use XML, and some may use different formats entirely. Also, not all APIs are set up to receive requests from web browsers, as the most common way to access an API is from a web server. Some APIs use the CORS(Cross-Origin Resource Sharing) standard to enable browsers to use its API (a good explanation of what CORS is can be found here; some other APIs use the JSONP approach for web browsers; and some APIs don't support web browser access at all.