How to build a large Angular.js application
Last editedJun 2024
At GoCardless, Angular.js has been in production since March this year. We wanted to share some of the things we've learned while building and maintaining a fairly large single-page application (SPA). The core app is 9K lines of code.
This post is split into two sections: why we chose Angular and best practices that have worked for us during development.
Project background
Angular came up when discussing a new project early this year, a project that started as a way to make our existing PayLinks tool more flexible, but became a full dashboard re-write.
Our legacy dashboards are stiched together with lots and lots of jQuery selectors on top of Rails. PayLinks was built using Backbone and served from within our legacy dashboards. Maintaining a SPA within server generated views and seperate, often conflicting, jQuery logic had become too painful.
Why Angular.js?
At GoCardless we write a lot of specs, but when it came to our front end we had all but skipped unit testing and settled with high level integration testing. The front end web development community has only recently started taking testing seriously. Tooling has been poor and frameworks have not been built with testing in mind.
Writing testable javascript without a solid framework and good tooling in place is just too hard. In practice you often rush to get stuff done. Add some jQuery selectors here and there, stick it in application.js and be done with that fix. What you often end up with is code that is almost impossible to test. Do I mock the entire page html in the spec? What data do I pre-popluate it with?... and so on. Messy code, even messier tests.
Angular.js is built from the ground up with testing in mind. In our opinion this makes Angular different from all other frameworks out there. It is the reason we chose it.
This testability is due to the feature set of Angular and the tooling available. Feature wise, dependency injection (DI), modules, directives, data binding, and the internal event loop all work together to create a testable architecture.
Angular maintains its own event loop outside the browser event loop to do dirty checking and make sure data is in sync. It will check all known objects for changes on every loop tick. This is done asynchronously. Because this loop is maintained by Angular, you can flush the queue of any outstanding request or pending change at any time, meaning you can test async code in a synchronous manner.
The test runner Karma, makes testing directives exceptionally easy. It transparently loads your templates as scripts and exposes them as Angular modules. You can use the same concept to package your app for production.
With DI you only ask for instances to be provided to you, DI figures out how to actually create them. Making it easy to mock/decorate in tests.
Thanks to Angular's testability we've been able to keep our app maintainable even as it's grown to 9K lines of code. We've also made a number of changes to the way we work with the framework which have improved maintainability, described below.
Organising your files
Most project scaffolds, tutorials, and sample apps have a folder for each type of file. All controllers in one folder, all views in another, and so on.
After a lot of experimenation with a folder structures, we found ourselves putting all related and dependent files in the same component/page folder as shown below.
You know where to look. And more importantly you know where and how to add stuff. Everything is logically grouped with closely associated files.
Our structure is still a work in progress however, and the next step for us is to move specs into each page/component folder, as well as css styles.
Modules and dependencies
Most sample Angular apps have a single 'app' module namespace and all other files use this module to declare themselves.
The downside of this is implicit dependencies: it becomes hard to track down where stuff is coming from and what dependencies the part you are working on has. Bad news for maintenance. You also have to make sure you load all your modules in the right order. A pain in testing where you just want to glob load all the files.
After realising this we moved to one module per file, both for app code and templates.
Here's an example of a router for one of our show pages:
angular.module('gc.customerPlans', [
'gc.customerPlansController',
'gc.plansService',
'plans/customer-plans-template.html'
]).config([
'$routeProvider',
function customerPlansRoute($routeProvider) {
$routeProvider.when('/customers/:id/plans', {
controller: 'CustomerPlansController',
templateUrl: 'plans/customer-plans-template.html',
resolve: {
plans: ['$route', 'PlansService',
function plansResolver($route, PlansService) {
return PlansService.findAll({
customer_id: $route.current.params.id
}).$promise;
}
]
}
});
}
]);
For us, the explicit dependencies are a huge win. It also means that you can be very specific of which module to load in your unit tests and whenever you start working on some code you haven't touched in a while, you immediately know what components are in use.
The angular-app project is a great example of this in the wild.
Testing setup
We currently use Karma as a test runner for our unit tests. Our dashboard app has around 500 expectations, with spec coverage at 77%. A full run takes around 1s using Karma and PhantomJS. If you're not using Karma, you should.
For integration testing we're using Protractor. This project will replace what is currently the Angular Scenario runner. An awesome project with less awesome docs.
See our configs for each at the bottom of the post.
Replacing Rails
We used to have all app assets served by Rails. Not a great static file server, single threaded and synchronous. Page reloads in development were taking 15s!
After some consideration, we skipped Rails entirely for assets and now use Grunt instead. Full page reloads now take ~1.5s. Scss and Angular templates are compiled on the fly in development.
In development a connect server is run with the compilation middleware. A Grunt task then generates an index.html file that references all our app assets.
Rails is still used to serve the index.html page due to auth being done entirely in the backend. At some point Rails will only be an API to the front end.
Conclusion
So far, we're really happy with Angular: it's been easy to develop with and maintain for the last 6 months.
We also plan on fully splitting the front end from our Rails app. Something we'll definitely blog about - and also open sourcing a lot of our custom components, moving them to installable bower packages.
For now, most of our config and setup are below. In the near future we'll release a seed project that incorporates this.