6 min read
http://www.flickr.com/photos/51035725600@N01/170795987/
Next in trending

Scalable code organization in AngularJS

Implement a modular application architecture

Scalable code organization in AngularJS

Implement a modular application architecture


This is the first in a series of articles about the best practices I’ve learned to apply when working on single-page applications with AngularJS. It started out as a basic list of best practices for some colleagues who were just getting to know the framework. This series will provide in-depth information and arguments supporting my tips. As the title of the series suggests, this stuff is based on opinions, both other developers’ and my own. I may be wrong, others may be wrong. Suggestions for improvement are welcome.

Organizing your code

A lot of the structure of your application is defined by the way your files are organized. AngularJS doesn’t prescribe a certain directory structure, but there are three commonly used patterns.

The file pattern: a file for each type of object

This approach is the one you too often find in sample projects, plunks and beginner tutorials. It was popularized by the angular-seed project and basically means you have no structure whatsoever. Here’s what it looks like:

app
assets
(images etc.)
partials
(directive and ng-include templates)
scripts
app.js
controllers.js
directives.js
filters.js
services.js
styles
(sass/less/css files)
tests
config
e2e
unit
views
(route templates)
index.html

The problem with this approach is that it simply doesn’t scale. As your project grows you quickly end up with enormous files and you find yourself scrolling up and down a lot to find what you’re looking for. You’ll quickly start to look for an alternative which you’ll find in the folders pattern.

The folders pattern: a directory for each type of object

The file pattern naturally evolves into separate files grouped together in directories, still following the same grouping. Instead of having a file for each object type, you have a directory for each of these types:

scripts
controllers
login-controller.js
directives
dropdown-menu.js
filters
fancy-value.js
services
auth-service.js
app.js

This approach will scale a lot better and will actually work quite well if you have no more than a dozen files in any of those directories. You may even get away with it for larger projects by using subdirectories to limit the number of files in one directory. Still, it does nothing to help you find a certain piece of code if you don’t know where to look and it certainly doesn’t indicate any relationship between files.

The module pattern: a module for each feature

Instead of grouping files by their type and kind (e.g. controllers, directives, services), the recommendation is to group files by feature. If you’re starting a new project, I recommend using ng-boilerplate as a starting point. Features are the “high-level sections of the application, often corresponding to top-level routes”. Each feature gets its own directory containing all of the logic, routes, templates, styles and tests directly related to the feature. The feature also gets its own module definition, similar to the one in app.js, so it can have its own configuration. Using this structure has several advantages:

  • Less coupling. Because of the strict separation of logical blocks of code, cross-referencing between features is discouraged. If you’re going to have a feature that depends on another feature, you’ll have to explicitly specify this by having one module require another. It’s a little more work, but you’ll think twice before doing so and it makes the relationship a lot easier to spot.
  • More confidence. If someone unfamiliar with the project has to fix a bug, he’ll have less trouble comprehending the implications of his changes since dependencies are easy to spot.
  • Less mocking. Writing unit tests becomes a lot easier because the reduced number of dependencies means there’s less stuff you need to mock in your tests. Since each feature is a module, you can test them as if they’re a separate application.
  • More code re-use. By grouping code feature-wise, it becomes a no-brainer to copy a whole feature from one project to another since you can simply take the whole directory. With the folders pattern this was not so simple because you’d have to look in several places to find the right files. It was easy to forget a file or not think to include a dependency.
  • Less browsing for files. Your workflow is simplified because you won’t have to look far for related files. Everything you need for a feature is located in one directory, making switching between them very simple.

Here’s an example of such a structure in action:

app
about
about.html
about.js
about.spec.js
home
home.html
home.js
home.spec.js
app.js
app.spec.js
assets
(images etc.)
common
directives
  resources (or models)
  services
styles
(sass/less/css files)
index.html

The most important change is the introduction of a module file for each feature: about.js and home.js. They are similar to app.js in that they define a new Angular module. A big change is that this will also be the location of any route related to the feature, meaning your route definitions will no longer be in one place, preventing app.js from becoming unmanageably large. It’s a good practice to use dot notation in the module name as a namespace and to indicate a relationship (but not a dependency):

angular.module('myApp.home', [ ])
.config(function ($routeProvider) {
$routeProvider.when('/home',
templateUrl: 'home/home.html',
controller: 'HomeController',
resolve: {
newsItems: function ($http) {
return $http.get('api/news');
}
}
});
})
.controller('HomeController', function ($scope, newsItems) {
$scope.newsItems = newsItems;
});

A good reference application following this structure is angular-app. One of the things I don’t feel good about is that the controllers are included in the same file as the module definition. Controllers can get quite large and so can routes if you have a lot of stuff to resolve. I suppose you have to decide for yourself if that’s acceptable. Otherwise you may choose to put controllers in their own file, perhaps even grouped together in a controllers directory.

If you look at angular-app’s common directory, you’ll notice it’s a hybrid of the folders pattern and the module pattern. The directives, resources (RESTful models) and services directories are simply collections of stand-alone objects, while security is a module. My suggestion is to put all your modules in the app directory and avoid creating this hybrid.

Another change worth mentioning is the location of the unit tests. In the example above you’ll see the tests (*.spec.js) are located alongside the script they belong to instead of in a separate tests directory. Having your tests in the same directory as their counterparts means they are easy to locate and promotes that they are updated as the script to be tested changes. However it will be a little more difficult to setup your testing tool (Karma) and build tool (Grunt) so tests will be run but not included in the build. In the end I think it comes down to personal preference as both approaches have their strengths and weaknesses.

Migrating to the modular architecture

Many projects start out with the folders pattern but end up being more suited for the modular architecture. If you find yourself wanting to migrate from the folders pattern to the module pattern, here’s some advice.

  • Create a branch. This is just common software engineering practice. Major changes like these should always be done on a branch. Better yet, use branches and pull requests for every chunk of work you do, but that’s another story.
  • Work feature-by-feature. Don’t try to rearrange the whole application overnight. You’ll end up breaking the entire application and spending hours trying to fit all the pieces back together. Debugging is hard if you’ve changed pretty much everything. It’s easier to just take one feature, create a module for it, move it’s parts into place and make sure it works. Then move to the next feature. Remember, it’s an iterative process.
  • If you’re unsure if something is part of a feature or not, leave it. You can always move it later. Start with the big and obvious chunks of work.

Next up in the series: Advanced routing