#native_company# #native_desc#
#native_cta#

JSONP Example with Observables


Learning Objectives

  • Know what JSONP is and how it overcomes CORS.

  • Know how to make JSONP requests in Angular.

What is JSONP?

JSONP is a method of performing API requests which go around the issue of CORS.

This is a security measure implemented in all browsers that stops you from using an API in a potentially unsolicited way and most APIs, including the iTunes API, are protected by it.

Because of CORS if we tried to make a request to the iTunes API URL with the Http client library the browser would issue a CORS error.

The explanation is deep and complex but a quick summary is that unless an API sets certain headers in the response a browser will reject the response, the iTunes API we use doesn’t set those headers so by default any response gets rejected.

So far in this section we’ve solved this by installing a chrome plugin which intercepts the response and adds the needed headers tricking the browser into thinking everything is ok but it’s not a solution you can use if you want to release you app to the public.

If we switch off the plugin and try our application again we get this error in the console

XMLHttpRequest cannot load https://itunes.apple.com/search?term=love&media=music&limit=20. No 'Access-Control-Allow-Origin' header is present on the requested resource. Origin 'http://run.plnkr.co' is therefore not allowed access.

JSONP is a solution to this problem, it treats the API as if it was a JavaScript file. It dynamically adds https://itunes.apple.com/search?term=love&media=music&limit=20 as if it were a script tag in our our HTML like so:

<script src="https://itunes.apple.com/search?term=love&media=music&limit=20"></script>

The browser then just downloads the JavaScript file and since browsers don’t check for CORS when downloading JavaScript files it a works around the issue of CORS.

Imagine the response from the server was {hello:'world'}, we don’t just want to download the file because:

  1. The browser would download the JSON and try to execute it, since JSON isn’t JavaScript nothing would actually happen.

  2. We want to do something with the JSON once it’s downloaded, ideally call a function passing it the JSON data.

So APIs that support JSONP return something that looks like JavaScript, for example it might return something like so:

process_response({hello:'world'});

When the file is downloaded it’s executed as if it was JavaScript and calls a function called process_response passing in the JSON data from the API.

Of course we need a function called process_response ready and waiting in our application so it can be called but if we are using a framework that supports JSONP these details are handled for us.

That’s JSONP in a nutshell.

  1. We treat the API as a JavaScript file.

  2. The API wraps the JSON response in a function who’s name we define.

  3. When the browser downloads the fake API script it runs it, it calls the function passing it the JSON data.

We can only use JSONP when:

  1. The API itself supports JSONP. It needs to return the JSON response wrapped in a function and it usually lets us pass in the function name we want it to use as one of the query params.

  2. We can only use it for GET requests, it doesn’t work for PUT/POST/DELETE and so on.

Refactor to JSONP

Now we know a little bit about JSONP and since the iTunes API supports JSONP let’s refactor our observable application to one that uses JSONP instead.

For the most part the functionality is the same as our Http example but we use another client lib called Jsonp and the providers for our Jsonp solution is in the JsonpModule.

So firstly let’s import the new JsonpModule and replace all instances of Http with Jsonp.

import {JsonpModule, Jsonp, Response} from '@angular/http';
.
.
.
@NgModule({
  imports: [
    BrowserModule,
    ReactiveFormsModule,
    FormsModule,
    JsonpModule (1)
  ],
  declarations: [AppComponent],
  bootstrap: [AppComponent],
  providers: [SearchService]
})
class AppModule { }
1 Add JsonpModule to the module imports.

Then let’s change our SearchService to use the Jsonp library instead of Http, like so:

@Injectable()
export class SearchService {
  apiRoot: string = 'https://itunes.apple.com/search';

  constructor(private jsonp: Jsonp) { (1)
  }

  search(term: string) {
    let apiURL = `${this.apiRoot}?term=${term}&media=music&limit=20&callback=JSONP_CALLBACK`; (2)
    return this.jsonp.request(apiURL)  (3)
        .map(res => {
          return res.json().results.map(item => {
            return new SearchItem(
                item.trackName,
                item.artistName,
                item.trackViewUrl,
                item.artworkUrl30,
                item.artistId
            );
          });
        });
  }
}
1 We injected Jsonp into the constructor and stored it on a property called jsonp.
2 We changed our apiURL by adding in callback=JSONP_CALLBACK.
3 We call jsonp.request(…​) instead of http.get(…​)

Important

The iTunes API supports JSONP, we just need to tell it what name to use via the callback query parameter.

We passed it the special string JSONP_CALLBACK.

Angular will replace JSONP_CALLBACK with an automatically generated function name every time we make a request

That’s it, the rest of the code is exactly the same and the application works like before.

Important

Remember if you installed the CORS plugin or added some other work around then you can switch them off, you don’t need them any more.

Summary

JSONP is a common solution in the web world to get around the issue of CORS.

We can only use JSONP with APIs that support JSONP.

To use JSONP in Angular we use the Jsonp client lib which is configured by importing the JsonpModule and adding it to our NgModule imports.

We make a JSONP request by calling the request function, it still returns an Observable so for the rest of your application that fact you are using Jsonp instead of Http should be invisible.

Listing

Listing 1. main.ts
import { NgModule, Component, Injectable } from "@angular/core";
import { BrowserModule } from "@angular/platform-browser";
import { platformBrowserDynamic } from "@angular/platform-browser-dynamic";

import {
  HttpClientJsonpModule,
  HttpClientModule,
  HttpClient
} from "@angular/common/http";
import { ReactiveFormsModule, FormControl, FormsModule } from "@angular/forms";
import {
  map,
  debounceTime,
  distinctUntilChanged,
  switchMap,
  tap
} from "rxjs/operators";

class SearchItem {
  constructor(
    public track: string,
    public artist: string,
    public link: string,
    public thumbnail: string,
    public artistId: string
  ) {}
}

@Injectable()
export class SearchService {
  apiRoot: string = "https://itunes.apple.com/search";

  constructor(private http: HttpClient) {}

  search(term: string) {
    let apiURL = `${this.apiRoot}?term=${term}&media=music&limit=20`;
    return this.http.jsonp(apiURL, "callback").pipe(
      map(res => {
        return res.results.map(item => {
          return new SearchItem(
            item.trackName,
            item.artistName,
            item.trackViewUrl,
            item.artworkUrl30,
            item.artistId
          );
        });
      })
    );
  }
}

@Component({
  selector: "app",
  template: `
<form class="form-inline">
  <div class="form-group">
    <input type="search"
           class="form-control"
           placeholder="Enter search string"
           [formControl]="searchField">
  </div>
</form>

<div class="text-center">
  <p class="lead" *ngIf="loading">Loading...</p>
</div>

<ul class="list-group">
  <li class="list-group-item"
      *ngFor="let track of results | async">
    <img src="{{track.thumbnail}}">
    <a target="_blank"
       href="{{track.link}}">{{ track.track }}
    </a>
  </li>
</ul>
 `
})
class AppComponent {
  private loading: boolean = false;
  private results: Observable<SearchItem[]>;
  private searchField: FormControl;

  constructor(private itunes: SearchService) {}

  ngOnInit() {
    this.searchField = new FormControl();
    this.results = this.searchField.valueChanges.pipe(
      debounceTime(400),
      distinctUntilChanged(),
      tap(_ => (this.loading = true)),
      switchMap(term => this.itunes.search(term)),
      tap(_ => (this.loading = false))
    );
  }
}

@NgModule({
  imports: [
    BrowserModule,
    ReactiveFormsModule,
    FormsModule,
    HttpClientModule,
    HttpClientJsonpModule
  ],
  declarations: [AppComponent],
  bootstrap: [AppComponent],
  providers: [SearchService]
})
class AppModule {}

platformBrowserDynamic().bootstrapModule(AppModule);

Caught a mistake or want to contribute to the book? Edit this page on GitHub!



Advanced JavaScript

This unique course teaches you advanced JavaScript knowledge through a series of interview questions. Bring your JavaScript to the 2021's today.

Level up your JavaScript now!