AngularJS Modular Architecture

In this post, I'd like to share some experience after setting up several enterprise-scale AngularJS projects for modular development, since I think the merits of this approach are quite universal.

If you're new to AngularJS, one of the best parts of the framework is its dependency injection.

Simply injecting the resources needed in a small project with several controllers, services, and directives is easy. But what about a larger projects, with hierarchical dependencies and multiple features? For simple projects, I often see this type of directory structure:

root  
|-- foo
|    |-- styles/
|    |-- assets/
|    |-- index.html
|    |-- scripts/ 
|    |    |-- controllers/
|    |    |    |-- controller-feature-a.js
|    |    |    |-- controller-feature-b.js
|    |    |-- directives/
|    |    |    |-- directive-feature-a.js
|    |    |    |-- directive-feature-b.js
|    |    |    |-- directive-feature-c.js
|    |    |-- services/
|    |    |-- app.js

This kind of structure, while fine for a simple application, becomes quickly unmanageable as a project grows. While logical, larger projects tend to be based around features. It's easy enough finding features a, b, and c within a few files. But as a project expands, as they often do, a more efficient strategy will become important, not only for simple folder organization but for modularization.

Given a module named foo, the module would have:

|-- foo
|    |-- assets/
|    |-- index.html
|    |-- scripts/ 
|    |    |-- feature-a/
|    |    |    |-- feature-a-controller.js
|    |    |    |-- feature-a-modal-controller.js
|    |    |    |-- feature-a-directive.js
|    |    |    |-- feature-a-interceptor.js
|    |    |    |-- feature-a-model.js
|    |    |    |-- feature-a-module.js
|    |    |    |-- html/
|    |    |    |    |-- feature-a.html
|    |    |    |    |-- feature-a-modal.html
|    |    |    |-- less/
|    |    |    |    |-- feature-a.less
|    |    |    |    |-- feature-a-modal.less
|    |    |-- feature-b/
|    |    |    |-- directive-feature-b.js
|    |    |-- app.js

Using this methodology, it's easy to see how much simpler it is to work within a given feature. The feature is modularized through the feature-module.js file, which might look something like:

angular.module('app.feature-a', [])  
  .constant('featureAConfig', {
    // probably best to put these in a separate file within the module
    TEMPLATE_URL: '/scripts/feature-a/html/',
    TEMPLATE_NAME: 'feature-a.html',
    CONTROLLER_NAME: 'FeatureACtrl',
    API_CONFIG: []
    // and so on
  })
  .config(function($routeProvider, featureAConfig) {
    $routeProvider
      .when('/featureA', {
        templateUrl: featureAConfig.TEMPLATE_URL + featureAConfig.TEMPLATE_NAME
        controller: featureAConfig.CONTROLLER_NAME
      });
  });

get automatic injection of these assets into your index.html file using Grunt, Gulp, or whatever build system you use. Once added, you can simply add your angular modules to the root Angular file as dependencies, like this:

angular  
  .module('app', [
    'ngAnimate', 
    'ngCookies', 
    'ngMessages',
    // etc...
    'app.featureA',
    'app.featureB',
    'app.featureC'
  ]); 

Keep it simple. There's lots you can do from here. You'll probably want to separate out core functionality, like authentication, into a separate feature area. You can throw your specs and tests there, or put them somewhere else entirely. Choices, choices.

Show Comments