Ben Nadel
On User Experience (UX) Design, JavaScript, ColdFusion, Node.js, Life, and Love.
Ben Nadel at CFUNITED 2010 (Landsdown, VA) with: Chris Peters
Ben Nadel at CFUNITED 2010 (Landsdown, VA) with: Chris Peters@cf_chrispeters )

Creating Specialized HTTP Clients In Angular 2 Beta 8

By Ben Nadel on

When I first started digging into Angular 2's Http client, I felt that a lot of the great Angular 1.x features were sorely missing. Features like automatic JSON (JavaScript Object Notation) parsing, request / response interceptors, and XSRF (Cross-Site Request Forgery) protection, to name a few. In a recent GitHub issue, Jeff Cross talks about why the Angular team kept the Http client so generic. And, at first, I completed disagreed with the mindset. But, after I've had some time to reflect on the matter, I have to say, I really like the decision that they made. It does make setting up an Http client a little more challenging; but, it puts you, as the developer, in total control over the way data flows into and out of a remote API. And, you never have to worry about messing up some other module's configuration; or, having some other module mess up your requests. In the end, it feels like this approach makes every aspect of an HTTP call much safer and easier to reason about.


 
 
 

 
 
 
 
 

Run this demo in my JavaScript Demos project on GitHub.

Every API has different constraints and expectations. Some use JSON, others use SOAP or XML. Some return their data in a response envelope, others return a naked response value. Some require basic authentication or an API key, others can be used anonymously. The point is, there's no "one size fits all" for API interactions. And, this is just as true for the consuming context. As such, it makes sense to create a specialized HTTP client to act as the HTTP gateway to a given API.

In the abstract, this sounds like a lot of work. It's easy to conjure up the image of creating dozens of specialized HTTP clients. But, in reality, this is not the case. In reality, you probably only talk to one or two APIs from a client-side application. And, in reality, the only API that has any real constraints is likely the one that you are providing to your own single-page application.

So, let's take a look at what a non-trivial specialized HTTP client might look like. In this demo, I'm going to try and create an ApiGateway service which is an HTTP implementation used specifically to talk to my own application's API. As a feature set I would like:

  • Encapsulation of the HTTP transportation mechanism (including Content-Type headers).
  • Param and Data driven URL interpolation.
  • Automatic JSON serialization for outgoing requests.
  • Automatic JSON parsing for incoming responses.
  • Normalized error responses.
  • XSRF (Cross-Site Request Forgery) protection.
  • Pending request tracking.
  • Explicit handling of certain HTTP response codes (ex, 401 Unauthorized).

Most of these features feel like they are directly related to how HTTP requests are made. All except for the last one. Responding to a specific type of HTTP status code feels like it's overloading the ApiGateway a little too much. Such logic feels like it should be slightly more external. As such, in my approach, rather than having the ApiGateway react to HTTP status codes, it simply exposes an "errors" RxJS stream. Then, anything in the application can subscribe to this error stream and implement additional reactive logic.

In the end, my demo architecture looks a little bit like this:


 
 
 

 
 Creating a specialized HTTP client in Angular 2 on top of the core HTTP service. 
 
 
 

My ApiGateway exposes the following public API:

  • errors - An RxJS observable emitting HTTP errors.
  • pendingCommands - An RxJS observable emitting the number of pending "command" (ie, non-GET) requests.
  • get() - A method for making GET requests.
  • post() - A method for making POST requests.
  • request() - A more generic request method.

There's a lot of code in this demo because it's implementing a lot of business requirements for API interaction. I've tried to put a lot of comments in the code. But, I also walk through the code in the video (well, mostly just blather on about the ApiGateway for 13 minutes - sorry, didn't know how to gather my thoughts well).

  • <!doctype html>
  • <html>
  • <head>
  • <meta charset="utf-8" />
  •  
  • <title>
  • Creating Specialized HTTP Clients In Angular 2 Beta 8
  • </title>
  •  
  • <link rel="stylesheet" type="text/css" href="./demo.css"></link>
  • </head>
  • <body>
  •  
  • <h1>
  • Creating Specialized HTTP Clients In Angular 2 Beta 8
  • </h1>
  •  
  • <my-app>
  • Loading...
  • </my-app>
  •  
  • <!-- Load demo scripts. -->
  • <script type="text/javascript" src="../../vendor/angularjs-2-beta/8/es6-shim.min.js"></script>
  • <script type="text/javascript" src="../../vendor/angularjs-2-beta/8/Rx.umd.min.js"></script>
  • <script type="text/javascript" src="../../vendor/angularjs-2-beta/8/angular2-polyfills.min.js"></script>
  • <script type="text/javascript" src="../../vendor/angularjs-2-beta/8/angular2-all.umd.js"></script>
  • <!-- AlmondJS - minimal implementation of RequireJS. -->
  • <script type="text/javascript" src="../../vendor/angularjs-2-beta/8/almond.js"></script>
  • <script type="text/javascript">
  •  
  • // Here, we're programmatically adding the XSRF Token cookie so that it will be
  • // reflected in the outgoing HTTP requests in the API Gateway.
  • document.cookie = "XSRF-TOKEN=Dont-Tase-Me-Bro";
  •  
  •  
  • // --------------------------------------------------------------------------- //
  • // --------------------------------------------------------------------------- //
  •  
  •  
  • // Defer bootstrapping until all of the components have been declared.
  • // --
  • // NOTE: Not all components have to be required here since they will be
  • // implicitly required by other components.
  • requirejs(
  • [ /* Using require() for better readability. */ ],
  • function run() {
  •  
  • ng.platform.browser.bootstrap(
  • require( "App" ),
  • [
  • ng.http.HTTP_PROVIDERS,
  • require( "FriendService" ),
  • require( "ApiGateway" ),
  • require( "HTTP_ERROR_HANDLER_PROVIDERS" )
  • ]
  • );
  •  
  • }
  • );
  •  
  •  
  • // --------------------------------------------------------------------------- //
  • // --------------------------------------------------------------------------- //
  •  
  •  
  • // I provide the root App component.
  • define(
  • "App",
  • function registerApp() {
  •  
  • // Configure the App component definition.
  • ng.core
  • .Component({
  • selector: "my-app",
  • template:
  • `
  • <p>
  • <a (click)="loadFriend( 1 )">Load Friend 1</a>
  • &nbsp;|&nbsp;
  • <a (click)="loadFriend( 2 )">Load Friend 2</a>
  • &nbsp;|&nbsp;
  • <a (click)="loadFriend( 3 )">Load Friend 3</a>
  • &nbsp;|&nbsp;
  • <a (click)="loadFriend( 4 )">Load Friend 4 (Not Found)</a>
  • &nbsp;|&nbsp;
  • <a (click)="updateFriend( 1 )">Update Friend 1</a>
  • </p>
  •  
  • <div *ngIf="friend">
  •  
  • <h3>
  • {{ friend.name }}
  • </h3>
  •  
  • <ul>
  • <li>
  • <strong>ID</strong>: {{ friend.id }}
  • </li>
  • <li>
  • <strong>Name</strong>: {{ friend.name }}
  • </li>
  • <li>
  • <strong>Description</strong>: {{ friend.description }}
  • </li>
  • </ul>
  •  
  • </div>
  • `
  • })
  • .Class({
  • constructor: AppController,
  •  
  • // Configure life-cycle methods on the prototype so that they
  • // are picked up at runtime.
  • ngOnInit: function noop() {}
  • })
  • ;
  •  
  • AppController.parameters = [
  • new ng.core.Inject( require( "FriendService" ) ),
  • new ng.core.Inject( require( "ApiGateway" ) )
  • ];
  •  
  • return( AppController );
  •  
  •  
  • // I control the App component.
  • function AppController( friendService, apiGateway ) {
  •  
  • var vm = this;
  •  
  • // I hold on the current friend-request subscription. This way, we
  • // can cancel this request if it has not completed by the time we
  • // need to initiate a new one (or destroy the component).
  • var currentSubscription = null;
  •  
  • // I hold the currently-loaded friend.
  • vm.friend = null;
  •  
  • // Expose the public methods.
  • vm.loadFriend = loadFriend;
  • vm.ngOnInit = ngOnInit;
  • vm.updateFriend = updateFriend;
  •  
  •  
  • // ---
  • // PUBLIC METHODS.
  • // ---
  •  
  •  
  • // I load the friend with the given ID.
  • function loadFriend( id ) {
  •  
  • // If we have an existing request subscription, cancel it.
  • // --
  • // NOTE: If the request already completed, there is no harm here.
  • if ( currentSubscription ) {
  •  
  • currentSubscription.unsubscribe();
  •  
  • }
  •  
  • // Request the new friend and keep track of the response
  • // subscription so that we can cancel it in the future.
  • currentSubscription = friendService
  • .getFriend( id )
  • .subscribe(
  • function handleValue( newFriend ) {
  •  
  • vm.friend = newFriend;
  •  
  • },
  • function handleError( error ) {
  •  
  • console.warn( "Could not load friend." );
  • console.dir( error );
  •  
  • }
  • )
  • ;
  •  
  • }
  •  
  •  
  • // I initialize the component, called after the inputs have been
  • // bound for the first time.
  • function ngOnInit() {
  •  
  • // Let's start listening for pending commands (ie, non-GET
  • // requests) so that we can show some sort of "activity"
  • // indicator if we wanted to.
  • apiGateway.pendingCommands.subscribe(
  • function handleValue( pendingCount ) {
  •  
  • console.debug( "Pending commands:", pendingCount );
  •  
  • }
  • );
  •  
  • }
  •  
  •  
  • // I update the friend with the given ID.
  • // --
  • // CAUTION: This won't work on GitHub pages - they don't allow
  • // posts. But, it should still manage the count of "pending commands"
  • // properly.
  • function updateFriend( id ) {
  •  
  • console.warn( "We are about to try a POST (this may not work in your environment)." );
  •  
  • friendService
  • .updateFriend( id, "Lisa" )
  •  
  • // We don't really care about the response for this demo;
  • // but, we have to subscribe in order to actually trigger the
  • // underlying HTTP request.
  • .subscribe(
  • function noop() {},
  • function noop() {}
  • )
  • ;
  •  
  • }
  •  
  • }
  •  
  • }
  • );
  •  
  •  
  • // --------------------------------------------------------------------------- //
  • // --------------------------------------------------------------------------- //
  •  
  •  
  • // I provide the friend service.
  • define(
  • "FriendService",
  • function registerFriendService() {
  •  
  • FriendService.parameters = [
  • new ng.core.Inject( require( "ApiGateway" ) )
  • ];
  •  
  • return( FriendService );
  •  
  •  
  • // I provide access to the friend repository by way of the API gateway.
  • // --
  • // NOTE: All internal access to the API is performed by the API gateway
  • // which encapsulates the HTTP mechanism and common configuration needed
  • // to communicate with the application's internal API.
  • function FriendService( apiGateway ) {
  •  
  • // Return the public API.
  • return({
  • getFriend: getFriend,
  • updateFriend: updateFriend
  • });
  •  
  •  
  • // ---
  • // PUBLIC METHODS.
  • // ---
  •  
  •  
  • // I retrieve the friend with the given ID.
  • function getFriend( id ) {
  •  
  • // NOTE: I could have hard-coded the "friends" token in the URL,
  • // but I wanted to demonstrate the URL interpolation that is
  • // being provided by the API gateway.
  • var stream = apiGateway.get(
  • "./api/:type/:id.json",
  • {
  • type: "friends",
  • id: id,
  •  
  • // Adding this to get more data in the query string for
  • // the purposes of the demo.
  • _cache: ( new Date() ).getTime()
  • }
  • );
  •  
  • return( stream );
  •  
  • }
  •  
  •  
  • // I update the friend with the given ID (with a new name).
  • function updateFriend( id, name ) {
  •  
  • // CAUTION: A post won't work in GitHub pages. But, it should
  • // still manage the changing count of pending requests.
  • var stream = apiGateway.post(
  • "./api/:type/:id.json",
  • {
  • type: "friends",
  • id: id,
  •  
  • // Adding this to get more data in the query string for
  • // the purposes of the demo.
  • _cache: ( new Date() ).getTime()
  • },
  • {
  • name: name
  • }
  • );
  •  
  • return( stream );
  •  
  • }
  •  
  • }
  •  
  • }
  • );
  •  
  •  
  • // --------------------------------------------------------------------------- //
  • // --------------------------------------------------------------------------- //
  •  
  •  
  • // I provide a custom HTTP gateway, specifically designed for consumption of
  • // the application's API. This gateway encapsulates the HTTP transportation
  • // mechanism and applies common configuration requirements such as JSON
  • // (JavaScript Object Notation) serialization and deserialization, XSRF token
  • // protection, and URL interpolation.
  • define(
  • "ApiGateway",
  • function registerApiGateway() {
  •  
  • ApiGateway.parameters = [
  • new ng.core.Inject( ng.http.Http )
  • ];
  •  
  • return( ApiGateway );
  •  
  •  
  • // I provide a custom implementation of the ExceptionHandler service
  • // that exposes an error stream that other services can subscribe to
  • // (via the public property).
  • function ApiGateway( http ) {
  •  
  • // I provide a beacon of HTTP errors that other parts of the
  • // application can listen to (and respond to if necessary).
  • var errors = new Rx.Subject();
  •  
  • // Keep track of all the non-GET requests that are being executed.
  • // This can be used to setup request indicators in the rest of
  • // the application.
  • var pendingCommandCount = 0;
  • var pendingCommands = new Rx.Subject();
  •  
  • // Return the public API.
  • return({
  • // Properties.
  • errors: Rx.Observable.from( errors ),
  • pendingCommands: Rx.Observable.from( pendingCommands ),
  •  
  • // Methods.
  • get: get,
  • post: post,
  • request: request
  • });
  •  
  •  
  • // ---
  • // PUBLIC METHODS.
  • // ---
  •  
  •  
  • // I perform a GET request to the API, appending the given params
  • // as URL search parameters. Returns a stream.
  • function get( url, params ) {
  •  
  • var stream = request({
  • method: ng.http.RequestMethod.Get,
  • url: url,
  • params: params
  • });
  •  
  • return( stream );
  •  
  • }
  •  
  •  
  • // I perform a POST request to the API. If both the params and data
  • // are present, the params will be appended as URL search parameters
  • // and the data will be serialized as a JSON payload. If only the
  • // data is present, it will be serialized as a JSON payload. Returns
  • // a stream.
  • function post( url, params, data ) {
  •  
  • if ( ! data ) {
  •  
  • data = params;
  • params = {};
  •  
  • }
  •  
  • var stream = request({
  • method: ng.http.RequestMethod.Post,
  • url: url,
  • params: params,
  • data: data
  • });
  •  
  • return( stream );
  •  
  • }
  •  
  •  
  • // I perform a request to the API using the given configuration
  • // options. The options can have the given format:
  • // --
  • // - options.method : string
  • // - options.url : string
  • // - options.headers : [{key: value},]
  • // - options.params : [{key: value},]
  • // - options.data : [{key: value},]
  • // --
  • // Returns a stream.
  • function request( options ) {
  •  
  • // Normalize the options config to ensure key exist.
  • // --
  • // CAUTION: This does not validate the keys, only makes sure
  • // they are added to the options if they didn't already exist.
  • options.method = ( options.method || ng.http.RequestMethod.Get );
  • options.url = ( options.url || "" );
  • options.headers = ( options.headers || {} );
  • options.params = ( options.params || {} );
  • options.data = ( options.data || {} );
  •  
  • // Move values from the PARAMS and DATA object into the URL where
  • // ever there are matching ":TOKEN" values. Any non-matching
  • // token (in the URL) will be replaced with the empty string.
  • // --
  • // Example: /users/:userID/friends/:friendID.json
  • interpolateUrl( options );
  •  
  • // Add the XSRF token header if the XSRF cookie is present.
  • addXsrfToken( options );
  •  
  • // For requests that might have a JSON body, we need to set the
  • // appropriate content-type header.
  • addContentType( options );
  •  
  • // Translate the ApiGateway options into actual HTTP request
  • // options that the Http client can consume.
  • var requestConfig = new ng.http.Request({
  • method: options.method,
  • url: options.url,
  • headers: options.headers,
  • search: buildUrlSearchParams( options.params ),
  • body: JSON.stringify( options.data )
  • });
  •  
  • // Before we initiate the request, let's determine if this
  • // request should to be considered a "command" that we need to
  • // keep track of.
  • var isCommand = ( options.method !== ng.http.RequestMethod.Get );
  •  
  • // If the request is a command, let's emit the new pending
  • // command count.
  • if ( isCommand ) {
  •  
  • pendingCommands.next( ++pendingCommandCount );
  •  
  • }
  •  
  • // Initiate API request.
  • var stream = http.request( requestConfig )
  • // While the ApiGateway will do nothing more than unwrap
  • // errors for the current request, it's likely that certain
  • // errors (such as 401 Unauthorized errors or 500 Server
  • // Errors) may require more "business logic" attention. As
  • // such, we're going to emit any error that comes through.
  • // --
  • // NOTE: The reason this gets its own catch-handler is to
  • // ensure that it is only dealing with HTTP errors and never
  • // with errors that may be thrown during response parsing.
  • .catch(
  • function handleHttpError( error ) {
  •  
  • errors.next( error );
  •  
  • return( Rx.Observable.throw( error ) );
  •  
  • }
  • )
  • // When the response comes back, we don't want to return
  • // the raw HTTP response. Instead, we want to unwrap both
  • // successful and error values, ensuring a normalized response.
  • .map( unwrapHttpValue )
  • .catch(
  • function handleError( error ) {
  •  
  • return( Rx.Observable.throw( unwrapHttpError( error ) ) );
  •  
  • }
  • )
  • // After the request is done, we need to clean up our pending
  • // command count (if this request was a command).
  • .finally(
  • function cleanup() {
  •  
  • if ( isCommand ) {
  •  
  • pendingCommands.next( --pendingCommandCount );
  •  
  • }
  •  
  • }
  • )
  • ;
  •  
  • return( stream );
  •  
  • }
  •  
  •  
  • // ---
  • // PRIVATE METHODS.
  • // ---
  •  
  •  
  • // I add the appropriate "Content-Type" header based on the type of
  • // request being performed.
  • function addContentType( options ) {
  •  
  • if ( options.method !== ng.http.RequestMethod.Get ) {
  •  
  • options.headers[ "Content-Type" ] = "application/json; charset=UTF-8";
  •  
  • }
  •  
  • return( options );
  •  
  • }
  •  
  •  
  • // I add the "X-XSRF-TOKEN" header if the "XSRF-TOKEN" cookie is
  • // present.
  • // --
  • // CAUTION: The name of the cookie is case-sensitive.
  • function addXsrfToken( options ) {
  •  
  • var xsrfToken = getXsrfCookie();
  •  
  • if ( xsrfToken ) {
  •  
  • options.headers[ "X-XSRF-TOKEN" ] = xsrfToken;
  •  
  • }
  •  
  • return( options );
  •  
  • }
  •  
  •  
  • // I build the URLSearchParams instance from simple key-value pairs.
  • function buildUrlSearchParams( params ) {
  •  
  • var searchParams = new ng.http.URLSearchParams();
  •  
  • for ( var key in params ) {
  •  
  • searchParams.append( key, params[ key ] )
  •  
  • }
  •  
  • return( searchParams );
  •  
  • }
  •  
  •  
  • // I delete the given key from the given collection and return the
  • // associated value.
  • function extractValue( collection, key ) {
  •  
  • var value = collection[ key ];
  •  
  • delete( collection[ key ] );
  •  
  • return( value );
  •  
  • }
  •  
  •  
  • // I extract the XSRF-TOKEN cookie (or empty string).
  • function getXsrfCookie() {
  •  
  • var matches = document.cookie.match( /\bXSRF-TOKEN=([^\s;]+)/ );
  •  
  • try {
  •  
  • return( matches && decodeURIComponent( matches[ 1 ] ) );
  •  
  • } catch ( decodeError ) {
  •  
  • return( "" );
  •  
  • }
  •  
  • }
  •  
  •  
  • // I interpolate the URL option, replacing ":TOKEN" patterns with
  • // matching values from the params and data structure. Any matching
  • // value is MOVED from the given hash to the URL.
  • function interpolateUrl( options ) {
  •  
  • options.url = options.url.replace(
  • /:([a-zA-Z]+[\w-]*)/g,
  • function replaceToken( $0, token ) {
  •  
  • // Try to move matching token from the params collection.
  • if ( options.params.hasOwnProperty( token ) ) {
  •  
  • return( extractValue( options.params, token ) );
  •  
  • }
  •  
  • // Try to move matching token from the data collection.
  • if ( options.data.hasOwnProperty( token ) ) {
  •  
  • return( extractValue( options.data, token ) );
  •  
  • }
  •  
  • // If a matching value couldn't be found, just replace
  • // the token with the empty string.
  • return( "" );
  •  
  • }
  • );
  •  
  • // Clean up any repeating slashes.
  • options.url = options.url.replace( /\/{2,}/g, "/" );
  •  
  • // Clean up any trailing slashes.
  • options.url = options.url.replace( /\/+$/g, "" );
  •  
  • return( options );
  •  
  • }
  •  
  •  
  • // I unwrap the HTTP error response, ensuring that a normalized JSON
  • // object is available to represent the error.
  • function unwrapHttpError( error ) {
  •  
  • try {
  •  
  • return( error.json() );
  •  
  • } catch ( jsonError ) {
  •  
  • return({
  • code: -1,
  • message: "An unexpected error occurred."
  • });
  •  
  • }
  •  
  • }
  •  
  •  
  • // I unwrap the HTTP value response, returning the application-
  • // specific payload.
  • function unwrapHttpValue( value ) {
  •  
  • return( value.json() );
  •  
  • }
  •  
  • }
  •  
  • }
  • );
  •  
  •  
  • // --------------------------------------------------------------------------- //
  • // --------------------------------------------------------------------------- //
  •  
  •  
  • // I provide all of the services necessary to handle HTTP errors coming out of
  • // the ApiGateway, including the application initialization script that wires
  • // the HttpErrorHandler to the ApiGateway.
  • define(
  • "HTTP_ERROR_HANDLER_PROVIDERS",
  • function registerHttpErrorHandlerProviders() {
  •  
  • return([
  • require( "HttpErrorHandler" ),
  •  
  • // If the ApiGateway emits an error, we want to handle that using
  • // the HttpErrorHandler. However, we don't have any reason for a
  • // "component" in the rendered component tree to know about this
  • // service (at this time). As such, we'll force the loading of the
  • // HttpErrorHandler using the APP_INITIALIZER multi-collection
  • // (which is kind of like a "run block" from AngularJS 1.x).
  • ng.core.provide(
  • ng.core.APP_INITIALIZER,
  • {
  • useFactory: function( httpErrorHandler ) {
  •  
  • return(
  • function runBlock() {
  •  
  • console.info( "HttpErrorHandler initialized." );
  •  
  • }
  • );
  •  
  • },
  • deps: [ require( "HttpErrorHandler" ) ],
  • multi: true
  • }
  • )
  • ]);
  •  
  • }
  • );
  •  
  •  
  • // I provide the HttpErrorHandler (which is like an Http interceptor specifically
  • // for the ApiGateway).
  • define(
  • "HttpErrorHandler",
  • function registerHttpErrorHandler() {
  •  
  • HttpErrorHandler.parameters = [
  • new ng.core.Inject( require( "ApiGateway" ) )
  • ];
  •  
  • return( HttpErrorHandler );
  •  
  •  
  • // I handle HTTP errors coming out of the ApiGateway service. This way,
  • // we can apply special business rules to HTTP errors that may evolve
  • // independently of the ApiGateway.
  • function HttpErrorHandler( apiGateway ) {
  •  
  • // Subscribe to the errors stream coming from the ApiGateway.
  • // --
  • // NOTE: These the actual HTTP responses, not the unwrapped values.
  • apiGateway.errors.subscribe(
  • function handleValue( value ) {
  •  
  • console.group( "HttpErrorHandler" );
  • console.log( value.status, "status code detected." );
  • console.dir( value );
  • console.groupEnd();
  •  
  • // If the user made a request that they were not authorized
  • // to, it's possible that their session has expired. Let's
  • // refresh the page and let the server-side routing move the
  • // user to a more appropriate landing page.
  • if ( value.status === 401 ) {
  •  
  • window.location.reload();
  •  
  • }
  •  
  • }
  • );
  •  
  • }
  •  
  • }
  • );
  •  
  • </script>
  •  
  • </body>
  • </html>

Since there is so much code here, I won't try to dive into any of the actual implementation. But, I will say that having a specialized Http client specifically for communication with my application's remote API feels very clean. And, due to its isolation, it feels like I can grow and evolve this HTTP client without having to worry about creating unexpected side-effects for other modules in the application. All in all, I like the decisions that the Angular 2 team has made about reducing the core Http feature-set; and, I am enjoying the architectural constraints that this imposes (even if feels like a little more upfront work).




Reader Comments

@Cristovao,

Good question. If this were a production application, I would likely write it in TypeScript. However, for these demo explorations, I wouldn't know how to keep it all in one page and keep all the proper syntax coloring. Presumably, if it were all in one page, I'd have to have some sort of non-JavaScript script tag that would be transpiled at runtime (much like the type="text/jsx" script tag with React's JSXTransformer). And, in that case, it wouldn't get correct color coding in the Gist.

So, to keep it simple for demos, I use ES5 and will (theoretically) use TypeScript for production.

Plus, I think ES5 forces you to think more about the code itself, not about the technology.

Reply to this Comment

@Cristovao,

I really hope Ben's doesn't mind that I did this, but I just published an article where I convert this specific example to Typescript. If you want to check it out it's available here:

https://blog.sstorie.com/adapting-ben-nadels-apigateway-to-pure-typescript/

@BenNadel, if you are not okay with this please let me know and I can remove it...but I tried to be clear that this was all based on your original article. I just found it to be a good exercise to use this non-trivial example as a starting point for an angular 2 typescript app.

Reply to this Comment

@Sam,

Awesome stuff :D It's actually really good for me to see the TypeScript version because I don't truly know the TS syntax, so being able to see how it maps onto JavaScript is helpful.

It's interesting how you break out the Types for your version. While I don't use types in the JS (obviously), that is one thing that I really am enjoying about TypeScript - the types add a lot of documentation. Meaning, I can look at a method call and see what types it's expecting. Then, I can look up what those types look like and it really makes things a lot more clear.

Very cool to see!

Reply to this Comment

@Sam,

Also, that's really interesting about having to import the RxJS operators that you are using. Since I'm using the UMD versions of all the files, it just comes in a big barrel. I didn't realize that when its more modular, you actually have to include some of those things to make it work. Will definitely squirrel that away in the back of my head for when I run into the "ARRRRG, WHY YOU NO WORK!!!" moments :D

Reply to this Comment

Hi,
whether it is possible to check when the observable starts? (eg. finally operator for finish action) I need in the simplest way to get information when http subscription starts, without override html class or build wrapper for it.

Reply to this Comment

@Marcin,

If you want to know when an HTTP request is actually initiated, I don't think you can use the response stream directly since the values won't travel down the stream until the HTTP response comes back (in either success or failure). As such, you to use an additional means of announcing the HTTP start.

In my demo, I'm doing that through the use of the "pendingCommands" observable. Notice that my HTTP client exposes "pendingCommands" in addition to the request methods. This way, I can monitor the running requests in aggregate as opposed to just listening for one to come back.

Of course, in my case, I'm only considering non-GET requests to be "commands". But, you could easily include all requests in such an observable.

You could then take that one step further to create your own sort of "$.ajaxStart()" and "$.ajaxStop()" event handlers. Actually, that would be kind of an interesting post on its own.

Reply to this Comment

In the function interpolateUrl( options )

This line isn't correct as it wld make http:// or https:// to http:/ or https:/ as well and make the url incorrect.

// Clean up any repeating slashes.
options.url = options.url.replace( /\/{2,}/g, "/" );

Reply to this Comment

@Sankaranarayanan,

Ah, awesome catch! In my demo, I only use relative URLs so I didn't see that happening. Excellent!

Reply to this Comment

Post A Comment

You — Get Out Of My Dreams, Get Into My Comments
Live in the Now
Oops!
Comment Etiquette: Please do not post spam. Please keep the comments on-topic. Please do not post unrelated questions or large chunks of code. And, above all, please be nice to each other - we're trying to have a good conversation here.