A journey into the web...

Angular2, a rest client interface

UPDATE 2017/07/26 : Some breaking change were pushed since I wrote this article. I updated it, but I didn't test it, feel free to comment if you find any issue. Thanks to Facundo Donato for noticing the library update.

Today I wanted to make a REST client interface for my project. I looked first on the Internet but surprisingly I didn't find anything good enough. I'll explain what I made, but first, what is the goal of this "interface".

Goal

To sum up the goal in one sentence it would be : having a layer for http requests. With that said, the point is to have something that adds headers, manage request methods, gets the response and parses it without writing it again and again through angular services.

Requirement

First, I use a library that follows CRUD rules (which is not needed for this solution) and is up to date, toyanskiy/ngx-resource

npm install --save ngx-resource

Angular version 2.4.0 and later

I assume you have an angular2 project already setup, I'll only details how to make the REST interface, and how to use it.

Let's get started

Let's create a file, I named it rest-client.ts and I put it in a folder called app/shared (it depends on your design choice, but rest-client must be global). Here is the code in rest-client.ts

// app/shared/rest-client.ts

import {ResourceCRUD} from 'ngx-resource';

export class RestClient<TQuery, TShort, TFull> extends ResourceCRUD<TQuery, TShort, TFull> {

}

This class will be our REST interface. It extends Resource which is the ngx-resource class that has access to request properties such as headers and URL.

Now let's take a look at an angular service which uses our RestClient, for this example we will make an angular service to manage books, the file name would look something like book.service.ts

// app/books/book.service.ts
import {Injectable} from '@angular/core';
import {RequestMethod} from '@angular/http';

import {ResourceAction, ResourceMethod, ResourceParams} from 'ngx-resource';
import {RestClient} from '../shared/rest-client';

interface IQueryInput {
  page?: number;
  perPage?: number;
  dateFrom?: string;
  dateTo?: string;
  isRead?: string;
}

interface IBookShort {
  id?: number;
  cover?: string;
  title: string;
  author: string;
}

interface IBook extends IBookShort {
  summary?: string;
  publishedDate?: Date;
}

@Injectable()
@ResourceParams({
  url: '/books'
})
export class BookService extends RestClient {

  @ResourceAction({
    isArray: true,
    path: '/'
  })
  getBooks: ResourceMethod<IQueryInput, IBookShort[]>;

  @ResourceAction({
    path: '/{!id}'
  })
  getBook: ResourceMethod<{id: any}, IBook>;

  @ResourceAction({
    method: RequestMethod.Post
  })
  saveBook: ResourceMethod<IBook, any>;

  @ResourceAction({
    method: RequestMethod.Put,
    path: '/{!id}'
  })
  updateBook: ResourceMethod<IBook, any>;

  @ResourceAction({
    method: RequestMethod.Delete,
    path: '/{!id}'
  })
  deleteBook: ResourceMethod<{id: any}, any>;
}

ResourceMethod is an interface from ngx-resource. The method generics are respectively request body and response body. Note that the service extends RestClient, it is mandatory.

Here is an example about how to consume the service above

class BookComponent {

  constructor(private _bookService: BookService) { }

  doSmth() {
    // GET http://.../books?page=2&perPage=10
    let books: IBookShort[] = this._bookService.getBooks({page: 2, perPage: 10});

    // GET http://.../books/42
    let book: IBook = this._bookService.getBook({id: 42});

    // Since getBook({id: 42}) returns an asynchronous response (so does every requests)
    // We must use the observable to successfully update and then delete the book
    // from the response
    book.$observable
       .toPromise()
       .then(() => {
         book.title = 'Ainsi parlait Zarathoustra';

         // PUT http://.../books/42
         return this._bookService.updateBook(book).$observable.toPromise();
       })
      .then(() => {
        // DELETE http://.../books/42
        return this._bootService.deleteBook(book).$observable.toPromise();
      });

    let newBook: IBook = {
      title: 'The call of the Cthulhu',
      author: 'H. P. Lovecraft'
    }

    // POST http://.../books
    this._bookService.saveBook(newBook);
  }
}

Here, I mostly copied the documentation from ngx-resource github page.

Notice that I didn't put the full URL in the comments, it's because right now the service BookService don't have the URL, I just put '/books'. I could have put the entire URL in the decorator ResourceParams, but it would have mean that I had to put the URL for each service I create. To avoid this, let's come back to RestClient, we will override one method from Resource

export class RestClient<TQuery, TShort, TFull> extends ResourceCRUD<TQuery, TShort, TFull> {

  $getUrl(methodOptions?: any): string | Promise<string> {
    let resPath = super.$getUrl();
    return 'http://myurl.com/api' + resPath;
  }

}

That's it. getUrl is a method from Resource and it is used by the decorator ResourceAction. The method now get the URL we put, it is '/books' and concatenates this with the full URL. Now it does that for every service that extends RestClient.

The second thing I wanted to have is a global header for every service, for JWT authentication for example. Here is how I did this

export class RestClient<TQuery, TShort, TFull> extends ResourceCRUD<TQuery, TShort, TFull> {
  $getHeaders(methodOptions?: any): any {
    const headers: any = {};
    if (methodOptions.auth) {
      headers.Authorization = localStorage.get('token');
    }
    return headers;
  }

  $getUrl(methodOptions?: any): string | Promise<string> {
    let resPath = super.$getUrl();
    return 'http://myurl.com/api' + resPath;
  }

  $responseInterceptor(observable: Observable<any>, request: Request, methodOptions?: any): Observable<any> {
    return Observable.create((subscriber: Subscriber<any>) => {
      observable.subscribe(
        (res: Response) => {
          if (res.headers) {
            const newToken: string = res.headers.get('Authorization');
            if (newToken) {
              localStorage.setItem('token', newToken);
            }
          }
          const body = (<any>res)._body;
          subscriber.next(body ? res.json() : null);
        },
        (error: Response) => {
          // I also made a layer to parse errors
          subscriber.error(new Error(error.statusText));
        },
        () => subscriber.complete()
      );

    });
  }
}

I added two things. First getHeaders, it's exactly the same behavior than getUrl we saw earlier. For every service it will get the token from localStorage and put it in the Authorization header.

To make the auth works, you should specify the param auth when a resource requires an authentication. Let's say a user have to be authenticated when he wants a list of books, the resource method should look like the following

// book.service.ts

  @ResourceAction({
    isArray: true,
    path: '/',
    auth: true // It'll put the Authorization headers
  })
  getBooks: ResourceMethod<IQueryInput, IBookShort[]>;

The second new method is responseInterceptor which overrides the method from Resource (remember, RestClient extends Resource). responseInterceptor allows us to catch the request response before sending it to the service which made the request. By doing this, we can parse the body, checks values, find a header entry, and so on. In the example above, I used this method to get the header in order to automatically refresh the JWToken.

Last thing but no least, or nothing will work. Import the resource module in your app module such as

import {ResourceModule} from 'ngx-resource';

@NgModule({
  imports: [
    ResourceModule.forRoot()
  ]
})
export class AppModule {}

Conclusion

I hope this solution is simple enough and easy to use. Bear in mind this solution is only about communication between client and server. You should consider routing within the app which is independent from services.

By the way, if you come up with something better, let me know.

If you liked it, share it!