1. HarvardSEASDev
  2. Development Tools
  3. SEAS Node Angular Base

Overview

HTTPS SSH

SEAS Node.js / AngularJS Base Project

A base Node.js / AngularJS application for the development team at SEAS.

The goal of this project is to act as a quick and easy starting point for new applications. It aims to provide:

  • A reusable build process: vendor dependency management, LESS processing, ES6 processing, live reloading, compilation for production
  • Common functionality: CAS authentication, Express server, logging, MongoDB / Mongoose
  • Project organization: source folder organization, app organization by state
  • Code guidelines: JSHint, JSCS, JSDoc

Babel is used throughout the project, even in the build process itself, so feel free to make use of ES6 features everywhere.

Table of Contents

Requirements

In order to allow for sessions to be managed across multiple backend servers and/or processes, you will need to have a MongoDB server set up and running. Connection parameters to the MongoDB server should be set in src/server/config.js.

Setup

Install Node.js.

Install Bower and Gulp globally:

npm install -g bower gulp

Clone the Git repository and install the dependencies:

$ git clone git@bitbucket.org:harvardseasdev/seas-node-angular-base.git
$ cd seas-node-angular-base
$ npm install
$ bower install

Make a copy of the default configuration file and adjust settings for your environment:

$ cp src/server/config_default.js src/server/config.js

You will need to create a User object in your MongoDB users collection to allow for authentication. Model specs can be found in src/server/mongo/models/User.js.

This concludes initial setup. Read on for how to build the project and run the server.

Building

This project uses Gulp to run its build process. All build-related files are located in the gulp/ folder. The configuration file for the build process is gulp/config.js. Typically, this file is only modified to add/remove Bower dependencies.

The build process exposes the following commands:

npm run build
npm run watch
npm run dev
npm run compile
  • build: Builds the project for development and puts it in the BUILD_DIRECTORY (build/ by default).
  • watch: Runs build and then watches the client files. If any changes are made to client files, those files are rebuilt and a LiveReload is triggered. (It should be noted that the watch task only watches src/client/ and that changes in src/common/ will not trigger a rebuild.)
  • dev: Runs watch and additionally launches the server. If any changes are made to server files, the server is relaunched.
  • compile: Builds the project for production and puts it in the COMPILE_DIRECTORY (compile/ by default).

Running the Server

To run the server, simply use the following command:

npm start

The server will be started and logs will be stored in src/server/logs/.

If DEV_BUILD is set to true in src/server/config.js, it will serve static files from the build directory. If it is set to false, it will serve static files from the compile directory.

Source Code / Directory Structure

This section explains the methodology behind the way the source code is organized and acts as a reference for locating specific folders and files.

The directory structure is laid out as such:

src/
    client/
        components/
            AppClasses/
            AppData/
            AppDirectives/
            ...
        less/
            ...
        states/
            app.less
            app.js
            app.tpl.html
            ...
        index.less
        index.js
        index.html
    server/
        lib/
            Logger.js
        express/
            routes/
                ...
            index.js
        mongo/
            models/
                ...
            methods/
                ...
            index.js
        index.js
        boot.js
        worker.js
        server.js
        config_default.js
    common/
        ...

Client Files

src/client/ is where all of the client files are located and it is the directory upon which the build process operates. States are the core concept behind the organization of this directory.

The idea is that the files should be organized so that everything specific a single state is in one place: LESS, JS, and HTML. Furthermore, the directories should be organized so that they resemble the hierarchy of the states themselves.

The very base state is called app and its files are located in the src/client/states/ folder. All application states should have their own folder inside of the folder for their respective parent state. If you need to break out your state files into more than a single file of each type, you can come up with your own scheme for organizing these; the concept of hierarchical state folders should still be the main organization technique.

The following is an example of what a more nested state structure might look like:

states/
    states/
        admin/
            admin.less
            admin.js
            admin.tpl.html
        directory/
            group/
                person/
                    person.less
                    person.js
                    person.tpl.html
                group.less
                group.js
                group.tpl.html
            directory.less
            directory.js
            directory.tpl.html
    app.less
    app.js
    app.tpl.html

So where does everything else go? Everything that isn't a state is a component. Theoretically, many components could be reused across projects, but a component just has to be something that is not associated with a particular state and is required by the application (e.g. an interface for interacting with an API).

The components in the base project are organized categorically. Each category of component is its own Angular module and has a folder in src/client/components/.

The component categories in the base project are separated as follows:

  • AppClasses: This is just a module to import the class files from the src/common/ folder and convert them to Angular factories, so they can be injected as dependencies in other modules.
  • AppData: This module handles interactions with the API. It has some reusable components, like the LoadingTask and LoadingManager classes, and the corresponding loadingOverlay directive. It also contains the APIService, which acts as a single location for defining API interactions.
  • AppDirectives: This is a general module for holding all custom directives. This is arguably the most reusable component category, but it could also contain directives specific to the application (e.g. a form that needs to be used in multiple locations).

Do not feel restricted by the existing categories; these can be organized in any way you want.

Server Files

src/server/ is where all of the server files are located. Most of the server files export functions as to utilize dependency injection and maintain reusability.

In the base directory are the files index.js, server.js, and boot.js. An explanation of these files follows:

  • index.js: This is the entry file for starting the server. All it does is initialize Babel and then run boot.js. This is necessary because Babel cannot be initialized and used in the same file in which it is imported.
  • boot.js: This is the file that sets up the master process for the server. It employs Cluster to take advantage of multiple processors. It runs worker.js in each of these processes.
  • worker.js: Much like index.js, this is the entry file for starting a worker. All it does is initialize Babel and then run server.js. This is necessary because Babel cannot be initialized and used in the same file in which it is imported.
  • server.js: This is the true starting point for the server code, and where most high-level changes will be made. It makes the calls to set up all of the server components.

The server also comes with a singleton Logger, located at src/server/lib/Logger.js. This is set up as a global variable and should be used for all of the server side logging. It provides various log levels (error, warn, info, debug), appends useful time and stack information, and has nice color coding when writing to the console. It is also responsible for writing logs to the appropriate files.

Core components are the core concept behind the organization of this directory. Basically, each piece of the server stack has its own folder where it handles its own responsibilities. The two core components in the base project are Mongoose and Express.

The core components of the base project are as follows:

  • Mongoose: This component is responsible for connecting to MongoDB and setting up the required Mongoose models and methods.
    • src/server/mongo/models/ contains files that export Mongoose models. The filename should be the same as the name of model being exported.
    • src/server/mongo/methods/ contains files that export Mongoose methods for the models of the same name. Again, the filename should match the model name.
    • All of the models and methods in these folders are automatically scanned and registered with the Mongoose connection.
  • Express: This component is responsible for setting up the Express app and defining all of the routes that will be served.
    • src/server/express/routes/ contains organized subdirectories and files that set up the routes for the app. These can be organized in any way you want.

Other potential core components would be different data stores such as MySQL or Redis, or different communication protocols such as Socket.IO.

Common Files

src/common/ is where all JS files needed by both the client and the server are located. Typically these are classes or libraries that employ logic that must be maintained in both locations, avoiding duplication of code. The files within this directory can be organized in any way that makes sense for a given project.

Build Process

This section expands on the specific details of the build process and contains important information to keep in mind when making changes to the project.

Vendor Files

Vendor dependencies are installed using Bower but simply installing a Bower module will not include any files in the build process. The files that need to be included in the build process should be added to the VENDOR_FILES object in gulp/config.js, using paths relatives to bower_components/. They are grouped by ASSETS, CSS, and JS. This is typically the only thing that needs to be modified in the gulp/config.js file.

Asset Processing

Any files in the src/client/assets/ folder will simply be copied to the assets/ folder in the final build.

LESS Processing

All LESS files are compiled and run through Autoprefixer, so there is no need to use vendor prefixes with compatible properties.

Any URLs you need to reference in the CSS that are located in the src/client/assets/ folder should be referenced as though the assets folder were in the same directory (e.g. background-image: url('assets/bg.png');).

It is important to note that, by default, the client CSS is compiled from a single entry file: src/client/index.less. Your Francisco Cabrita calls from that file will determine which files are included in the compilation.

JS Processing

All client JavaScript is processed using Browserify and Babel, so using ES6 features is encouraged.

When the code is minified for production, ng-annotate is used so that the mangling of function arguments doesn't disrupt the automatic AngularJS dependency injection. In general, it can automatically detect when this is needed. However, if a function that relies on Angular dependency injection is imported from a different file, it must have a special comment preceding its declaration: /*@ngInject*/. See src/client/components/AppDirectives/ for examples of this.

It is important to know that, by default, the client JavaScript is compiled from single entry file: src/client/index.js. Your require calls from that file will determine which files are included in the compilation.

Also note that Browserify is set up to use src/ as a valid root, in addition to node_modules/ (e.g. if you want to require src/common/{filename}.js, you can use require('common/{filename}.js'); from anywhere).

HTML Processing

All of the HTML templates in the project have the extension .tpl.html. The inclusion of these files in the build process is automatic; all .tpl.html files within src/client/ are included in the build process.

The files themselves are not included in the final build, however. ng-html2js is used to convert the HTML files into strings that are stored inside an Angular module called "appTemplates". This lets us avoid making HTTP requests for individual template files. All of the files can be used as templates inside Angular by referencing their path relative to src/client/.

See the src/client/states/ directory for examples of how to set up states and their templates.

Conventions

This section explains some of the conventions used in the project, with the intention of providing a framework for consistency moving forward.

Angular and Browserify Modules

At first glance, the Angular module system and the Browserify module system would seem to be at odds; they both do similar things in different ways. One could potentially ignore the Angular module system and simply require() all of the necessary files. There is a convention established in this project that blends the two.

When listing an Angular module's dependencies, require() calls are used as follows:

angular.module('Module', [
    require('otherModule.js').name
]);

The imported file should export an Angular module. What the above code does is import the code from that file, making it a part of the codebase, and then simply take the name attribute from the imported module and give it to the new module as a dependency.

Angular Naming Conventions

The project employs a specific naming convention for both Angular modules and AngularUI Router states. Angular module names are capitalized and hierarchical. Thus, a module Team would have a child Team.Person. The same goes for state names. The state names for these would be team and team.person.

This is just a recommendation, although it should be noted that the state names do have a functional purpose; AngularUI Router states named in this way are automatically assigned the correct parent, instead of having to specify parent: 'team'.

AngularUI Router Views

AngularUI Router states use a ui-view directive to determine which element they bind to. This directive does not require an argument but can accept one to assign the associated view a name. If there is no name, states automatically populate any ui-view within their parent.

These named views would only be absolutely necessary in a case where two states exist alongside each other at the same level. Even though this is not the case in the base project, named views are always used to eliminate ambiguity. See the the views property of state definitions in this project for more information.

You can also read more about AngularUI views here.

LoadingTask / LoadingManager

Two useful classes are provided in the base project: LoadingTask and LoadingManager. These are designed to make API calls as simple and elegant as possible. Descriptions follow:

  • LoadingTask: Essentially a wrapper for an Angular HTTPPromise that implements a lot of common needs.
    • Makes API calls relative to the base API URL, eliminating the need for repetition of the full route.
    • Can automatically transform responses using a given function (e.g. instantiating a class using the retrieved data before resolving).
    • Handles the server's response and makes it nice for the promise resolution. On success, just resolves with the data itself instead of the full response. On failure, takes the error property of the returned object and rejects with just the error string.
    • See src/client/components/AppData/APIService.js for examples.
  • LoadingManager: A class to aggregate LoadingTasks and keep track of them as a group.
    • Provides methods for waiting for multiple LoadingTasks. More can be added and the LoadingManager will take them into account before resolving.
    • See src/client/index.js for example.
  • loadingOverlay: A directive to display the status of a LoadingManager.
    • When tasks are in progress, shows a loading spinner and a description of the tasks. Hides itself when no tasks are in progress.
    • See src/client/index.html for example.

The LoadingManager is also taken advantage of to implement sequential loading based on state. If you look at src/client/index.html, you'll see that an ng-if is used on the main ui-view. This ensures that no child state will be loaded before the LoadingManager is done loading. This allows for certain assumptions, like knowing you'll have access to the user data once the app state loads. This is a common pattern for state templates in this project:

<loading-overlay loading-manager="loadingManager"></loading-overlay>
<div id="app" class="fade" ng-if="appInitialized" ui-view="app"></div>

The scope variable appInitialized is just a boolean that is set to true when the LoadingManager first finishes loading (see src/client/index.js).

Linting / Code Style

JSHint is run on any JS that is processed. The build process will alert you of JSHint findings but these warnings will not crash the build. The settings for JSHint can be found in .jshintrc.

Although there are defaults in the JSHint settings, one thing that may need to be modified from time to time is the globals property. Normally JSHint will throw an error if you try to use a variable that is not defined in a given file. This globals property is an array of values that will not trigger this error. For example, "angular" needs to be included in this array, as it defined in the vendor file and then used globally.

Code style is merely a recommendation. It is not enforced in the build process. JSCS is used to define code style and the settings for JSCS can be found in .jscsrc.

Documentation

Documentation is not strictly enforced and no documentation generator is currently used but it is recommended that everything be documented following the conventions of JSDoc. For Angular-specific conventions, see ngDoc.

CAS Authentication

The Express server implements CAS authentication using the cas-authentication library, which was originally created specifically for this project. CAS is set up in src/server/express/index.js and implemented as middleware and endpoints in src/server/express/routes/index.js.

There is some additional work done with CAS authentication in src/server/express/routes/api/index.js. The middleware implemented there protects the entire API route:

  • If the user is not authenticated with CAS, it responds with a code 401 Unauthorized.
  • If user is authenticated and there is no stored session property user_accessLevel, it checks MongoDB for an associated User and stores this data.
  • If the user is authenticated with CAS but has no entry in MongoDB, it responds with a code 401 Unauthorized response.
  • If the user is authenticated and has an entry in MongoDB, it then passes through the whatever route the user was requesting.

On the client side, there is a file src/client/components/AppData/APIInterceptor.js. This is an Angular \$http interceptor. It intercepts all \$http requests and does the following:

  • On successful response:
    • Allows template file requests to pass through.
    • If there is an error property on the response data (this is how the server reports errors), returns a rejected promise with the error.
    • Otherwise, just returns the response data (so \$http requests will just return the data itself, and not the whole response object).
  • On error response:
    • If the response status is 401, redirects to the URL for CAS authentication.
    • Otherwise, just returns a rejected promise with an error string.

The redirection on 401 is important because the app can stay open, making API calls, and once it gets a 401 response it will redirect to the login. Otherwise, the server would have no way to force the client to go to the CAS login from an API call.

Most CAS configuration can be done in the server configuration file located at src/server/config.js.

When developing, CAS_DEV_MODE can be set to true and the server will pretend to do CAS authentication and use the CAS_DEV_USER string as the retrieved HUID.