3
\$\begingroup\$

Our system is largely a PHP-driven system. We're not doing the full split back/front-end. Most pages are plain HTML served directly by PHP. However, a few parts of the system are very dynamic and so are built with an Angular front-end.

A few relevant details:

  1. I wanted the transition from the PHP-driven website to the Angular app to be seamless for the user (i.e. they should not have to login again)
  2. Both PHP and Angular should be designed with the idea in mind of a future transition to a pure-Angular front-end. When that happens, I shouldn't have to re-write the Angular app or the PHP backend.
  3. The Angular app uses redux
  4. The Angular app does not have to do any routing.
  5. This particular app manages rules and actions for a rules engine. The details are fairly complicated, but also not entirely relevant. My primary concern here is over how the Angular app is organized and initialized, regardless of what exactly the app is actually doing.

Regarding the transition from PHP to Angular: keeping in mind a future goal of a completely split back and front-end, I wanted my app to communicate exclusively with REST APIs, and not to be using the cookie that the rest of the PHP application uses to store details about the logged in user. As a result, I have a special PHP end point that authenticates the user via their cookie and returns an API token. That API token is then stored and used by the Angular app for all other API calls in a RESTful fashion. As a result, the actual "startup" procedures for the Angular app are:

  1. POST to login API and fetch API Token
  2. POST to various endpoints to fetch all the configuration data needed for the app to do its job
  3. Use that data to populate the Redux store

Starting with my App component, here is some more relevant background:

  1. The API URL for all of this is not fixed: this application is used to manage the Rules engine for a variety of different modules, and each module has its own API URL with slightly different configuration details but the exact same API mechanics. As a result, the API URL is not hard-coded. In the development environment the API URL comes out of the environment, and in production it is passed down from the PHP page that launches the angular app via a simple global variable that is pulled out of the window object.
  2. All the services here are my action creators for the redux store. The various calls to service.fetchY() all take the configuration (which contains the ApiUrl and ApiToken) and make their own API call, updating the Redux store with the results.
  3. app.component.html is relatively empty. It just contains a small skeleton and then defers everything to a couple sub-modules that have main components which get their data out of the Redux store. As a result, app.component doesn't do much other than initializing the app. I've considered having it select different parts of the store to pass off to said sub-modules, but that will likely end up being the entire store, so I'd rather just let each sub-module pick out only the parts they need.

app.component.ts

import {
    Component,
    OnInit
} from '@angular/core';

import { select } from '@angular-redux/store';
import { IEnvironment } from './store/config/i-environment';
import { ConfigService } from './store/config/config.service';
import { RuleModelService } from './store/rule-model/rule-model.service';
import { FieldService } from './store/field/field.service';
import { PlaceHolderService } from './store/place-holder/place-holder.service';
import { WindowService } from './window.service';
import { environment } from '../environments/environment';

@Component({
    selector: 'my-app',
    templateUrl: './app.component.html'
})
export class AppComponent implements OnInit{

    @select() ruleModels$;

    constructor(
        private configService: ConfigService,
        private uiService: UiService,
        private windowService: WindowService,
        private ruleModelService: RuleModelService,
        private fieldService: FieldService,
        private placeHolderService: PlaceHolderService
    ){}

    ngOnInit(){
        // this pretty much starts the whole app.
        // in production, we get the ApiUrl out of the window, because
        // it is set as a variable in a <script> tag.  Otherwise these
        // things come out of our environment
        let environmentData: IEnvironment = Object.assign({}, environment);
        if (environmentData.production){
            environmentData.ApiUrl = this.windowService.window().ApiUrl;
        }

        // configService.getConfig() will call the login endpoint and get the API Token.
        // This is actually stored in the Redux store, but rather than subscribing
        // to the config portion of the Redux store, I had the configService also return a
        // promise that returns the configuration.  I do this simply to make the connection
        // between that first API call and the subsequent initialization steps more obvious.
        this.configService.getConfig(environmentData).then((config) => {
            this.ruleModelService.fetchRuleModels(config);
            this.fieldService.fetchFields(config);
            this.placeHolderService.fetchPlaceHolders(config);
        });
    }
};

store/config/config.service.ts

import { Headers, Http } from '@angular/http';
import { Injectable } from '@angular/core';
import { NgRedux } from '@angular-redux/store';
import { IConfig } from './i-config';
import { IEnvironment } from './i-environment';
import { IState } from '../i-state';
import { SET_CONFIG } from '../actions';

import 'rxjs/add/operator/toPromise';

@Injectable()
export class ConfigService {
    constructor(
        private ngRedux: NgRedux<IState>,
        private http: Http
    ) { }

    getConfig(environment: IEnvironment): Promise<IConfig> {
        let ApiUrl = environment.ApiUrl;

        // initialize an HTTP request to get the user's login credentials
        let headers = new Headers({
            'Content-Type':     'application/json'
        });

        // our API key is actually fetched via an HTTP request that relies on cookie-based auth.
        // This isn't ideal, but it is a temporary hack that helps with logins as we transfer back and
        // forth between the PHP-driven system and the angular driven system.  It will go away
        // once we switch fully to a split back and front end, and it shouldn't introduce any
        // actual security risks.
        return new Promise<IConfig>( ( resolve: Function, reject: Function ): void => {
            // In the development environment these details are set in our environment
            if ( !environment.production ) {
                let config: IConfig = {
                    ApiUrl,
                    RuleId: null,
                    ApiKey: environment.ApiKey,
                    MembershipId: environment.MembershipId
                };

                // update the redux store and resolve our promise
                this.setConfig( config );
                resolve( config );
                return;
            }

            this.http
                .post( `${ApiUrl}?route=login`, '', { headers: headers } )
                .toPromise()
                .then( ( response ) => {

                    let auth = response.json().data;

                    // get the data we care about out of the results
                    let config: IConfig = {
                        ApiUrl,
                        RuleId: null,
                        ApiKey: auth.ApiKey,
                        MembershipId: auth.MembershipId,
                    };

                    // update the redux store and resolve our promise
                    this.setConfig( config );
                    resolve( config );
                } )
                .catch( ( error ) => {
                    reject( error )
                } );
        } );
    }

    setConfig( config: IConfig ) {
        this.ngRedux.dispatch<any>( { type: SET_CONFIG, config } );
    }
}

store/place-holder/place-holder.service.ts

RuleService, PlaceHolderService, and FieldService are all nearly identical: just slightly different endpoints and interfaces. As a result, I'm only going to include one for the sake of space:

import { NgRedux } from '@angular-redux/store';
import { Injectable } from '@angular/core';
import { IState } from '../i-state';
import { IPlaceHolder } from './i-place-holder';
import { IConfig } from '../config/i-config';
import { Headers, Http } from '@angular/http';

import 'rxjs/add/operator/toPromise';

import { SET_PLACE_HOLDERS } from '../actions';

@Injectable()
export class PlaceHolderService{
    constructor(
        private ngRedux: NgRedux<IState>,
        private http: Http
    ){}

    setPlaceHolders(placeHolders: IPlaceHolder[]){
        this.ngRedux.dispatch<any>({type: SET_PLACE_HOLDERS, placeHolders});
    }

    fetchPlaceHolders(config: IConfig): void{
        // initialize an HTTP request to get the full series data
        let headers = new Headers({
            'Content-Type': 'application/json',
            'MembershipId': config.MembershipId,
            'Authorization': 'Bearer ' + config.ApiKey
        });

        this.http
            .get(`${config.ApiUrl}?route=get_placeholders`, { headers: headers })
            .toPromise()
            .then((response) => {
                this.setPlaceHolders(response.json().data.map((incoming: any): IPlaceHolder => {
                    return {
                        name: incoming.name,
                        label: incoming.label,
                    }
                }));
            })
            .catch(this.handleError);
    }

    handleError(error: any): void{
        console.error(error);
    }
}

I'm interested in any and all feedback, but here are some particular questions I have:

  1. I'm especially interested in any feedback on my initialization procedures: I ask the configService to get the application config (which primarily means the authentication token) and then use the result of that to trigger a call to the other endpoints, fully initializing the app. Is that reasonable?
  2. I am calling 3 different endpoints to get all the data I need. I had considered wrapping this all up in one endpoint that returns three different pieces of information. That would certainly involve less server calls (which is good), but also seems like a poor Separation of Concerns for an API endpoint (which is bad). Thoughts?
  3. These HTTP calls are mixed up inside my action-creator services. This seemed pretty reasonable to me, but I'm new to Redux and thought others might disagree. Am I being crazy here?
  4. The fetch methods in all the services both return promises that they resolve with the answer, and also update the store with the new data. This is certainly redundant. Obviously updating the store is a requirement. I could ditch the promise to minimize duplication (and I feel like that would be more in line with typical application flow), but in this one particular case I like being able to have that direct connection between "get some stuff" and "then do some more stuff". Is this a reasonable time to step outside of the norm?
  5. Is it reasonable to have a relatively empty app.component with most behavior being handled by some sub-modules that access the store directly? Or should I have the app.component fetch data out of the store and attach itself to the inputs and outputs of the other components that are exported by the sub-modules? These guys are fairly complicated, and there would be a lot of data flowing back and forth.
\$\endgroup\$
3
+200
\$\begingroup\$

I'm especially interested in any feedback on my initialization procedures: ... Is that reasonable?

As long as your code is dealing with configuration only, it looks good to me overall. (I assume, you don't have a login screen.) Otherwise, you may want to look at real life authentication example with Auth0. I'm not pointing to technology here, but rather how the concerns are being separated and organized.

I might rename ConfigService into EndpointApiConfigService to be a bit more specific, but I don't know the complete structure of the object and whether it holds any non-endpoint configuration or not.


I am calling 3 different endpoints to get all the data I need. I had considered wrapping this all up in one endpoint that returns three different pieces of information. That would certainly involve less server calls (which is good), but also seems like a poor Separation of Concerns for an API endpoint (which is bad). Thoughts?

This is always a non-trivial API design question to answer. There are many factors you should keep in mind and I will mention a couple of important once. BTW, I'm assuming we're talking about RESTful APIs here.

  • API performance and usage. This one you mention in your question directly. You don't mention the costs nor that rates of fetchRuleModels, fetchFields, fetchPlaceHolders invocations. If these are called very frequently, or they are pretty heavy, or both, it's may be a good reason to introduce an API that returns these data combined into a single document. Or you could use server side cache -- you have options here...
  • The previous question forces me to think about granularity of the resources. Your RESTful API may or may not become dirty if you combine APIs together for the sole sake of convenience. In my practice I try to find a name of the new resource that would natutally embrace all the concepts being united. If I can find one, so be it -- it's okay to have such resource, especially if it's a read-only projection of other resources, which seems to be your case unless ruleModels, fields, or placeHolders are non-readonly and/or changing frequently.

    I'd also recommend leaving the existing APIs intact. You may need/have other clients that don't want to deal with the composite resource. In other words, think about the actual and possible clients. Some of them will like to work with specific resource kinds. This may be critical when those resources have POST/PUT/DELETE. It's sometimes pain in the neck to work with these verbs on composites.

    RESTful Web Services Cookbook's chapter 2.4 covers this in more details.

    RESTful Web Services Cookbook

    Problem: You want to know how to provide a resource whose state is made up of states from two or more resources.

    Solution: Based on client usage patterns and performance and latency requirements, identify new resources that aggregate other resources to reduce the number of client/server round-trips.

  • Having a composite-based API may help achieve client code simplicity. If one of those fetchX APIs in your current implementation fails, your application will need to gracefully handle it and it's not quite obvious how exactly. From client code's perspective, it would be simpler to attempt to load the entire Config at once, and if it fails just show a generic "could not load config" or retry the entire thing.


These HTTP calls are mixed up inside my action-creator services. This seemed pretty reasonable to me, but I'm new to Redux and thought others might disagree. Am I being crazy here?

DISCLAIMER: I'm not very familiar with Redux. In my book triggering fetchX requests based on getConfig request completion is business logic. (Or even more generally: triggering "something" based on "something else" is business logic.)

If something is business logic, I'd rather make sure it is implemented somewhere in my reducer, rather than a service. So, either the getConfig() or its .then(...) would emit a proper action (CONFIG_LOADED in this case).


The fetch methods in all the services both return promises that they resolve with the answer, and also update the store with the new data. This is certainly redundant. Obviously updating the store is a requirement. I could ditch the promise to minimize duplication (and I feel like that would be more in line with typical application flow), but in this one particular case I like being able to have that direct connection between "get some stuff" and "then do some more stuff". Is this a reasonable time to step outside of the norm?

I think, this disconnect is non-Redux idiomatic. I may be wrong.

As soon as you use .then() or any other non-command/reducer code to express relationship between entities in your application, you get "broken" store in my understanding. For example, it becomes impossible to track the CONFIG_LOADED command. It simply does not exist. Similarly and more importantly, it is impossible to replay it if you wanted to debug it. It is also hard to imagine how to unit test it.

I understand that when everything is done in a clean redux/Event Store way, it may be hard to track what happens when. I'm still looking for a good way to visualize the flow.

Coming back to Is this a reasonable time to step outside of the norm? question. I think, it's your call. On one hand, it's probably uncool to "blindly" follow the pattern. On another hand, redux is done the way it's done for a reason. And as soon as you diverge from it, you establish a precedent that may then be used as an excuse to not do something else the "right" way. I prefer to stay away from such, but your code is your code.


Is it reasonable to have a relatively empty app.component with most behavior being handled by some sub-modules that access the store directly? Or should I have the app.component fetch data out of the store and attach itself to the inputs and outputs of the other components that are exported by the sub-modules? These guys are fairly complicated, and there would be a lot of data flowing back and forth.

In my experience it's best to make the components aware of the Stores. Wherever it is possible and straightforward. "Dumb"[er] components are the way to go.

Nowadays, I rarely use Input/Output, and only in scenarios of grid-alike components (which still sucks, and I'm rewriting those too).


One thing I wanted to mention is toPromise(). What is the point of it if you are not using await with Promises?

Knowing that async/await is not a recommended thing in ngOnInit (quite limiting, huh?) I don't see any gains here. Why not consider sticking to Observables which allow you a lot more than simple .then()? You only need to remember to .subscribe() on the end caller site, and use .map()/.flatMap() or .do() wherever it's absolutely necessary.


Another thing is this code:

    let headers = new Headers({
        'Content-Type': 'application/json',
        'MembershipId': config.MembershipId,
        'Authorization': 'Bearer ' + config.ApiKey
    });

    this.http.get(someURL, { headers: headers })
      ...
      .catch(this.handleError);

Normally, projects extract it into some kind of a CustomHttpService so that you don't need to create and pass those headers all over the place, or duplicate error handlers, or do any other repetitive code. Basically, DRY.


Hopefully, it's helpful. Don't take my words for granted! :)

\$\endgroup\$
  • \$\begingroup\$ This is all very helpful, thanks! I didn't think I was going to get anything, and this covers all my questions, so I'm going to go ahead and call it. \$\endgroup\$ – Conor Mancone Dec 27 '17 at 0:47
  • \$\begingroup\$ @ConorMancone I think for the bounty awarded you deserved a similar quality answer from someone more experienced with redux, that's why I waited till the very last day to post my thoughts. \$\endgroup\$ – Igor Soloydenko Dec 27 '17 at 4:18
  • \$\begingroup\$ I'm not worried in the slightest. I was looking for the kind of general overview which you provided, and your answer helped clarify a lot of my thoughts. I'm newer to Angular and Redux, but I'm an old hat at managing these kinds of things overall, so an answer like yours was perfect. \$\endgroup\$ – Conor Mancone Dec 27 '17 at 13:29
  • \$\begingroup\$ Regarding whether to return a promise or attach to the store, you are 100% right: making an exception from the rules should only be done when really necessary, and it isn't here. We haven't gotten into the replay properties of redux yet, but I'm a load-time nazi, so server-side rendering is definitely in our future and as a result I shouldn't be mucking around with breaking the "chain of command". I hadn't thought through that one fully. \$\endgroup\$ – Conor Mancone Dec 27 '17 at 13:30

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.