Structuring a modern web app, take two

Dario Gieselaar
5 min readNov 2, 2015

--

After working for a few years with a very basic concatenate and minify setup, it became very uncomfortable as our codebase grew. Our emergency plan? Burn it all, and Build It Better ™.

Background

The application we’re working on (Zaaksysteem), is a medium to large web app. It’s rendered server-side, and we render/decorate with Angular components. We use Gulp for concatenating and minifying JavaScript (and compiling/bundling scss) and Bower for pulling in libraries.

Lessons learned

This setup works well enough when the number of components you have is small, but it quickly broke down:

  • Dependencies are not well defined. There’s no way to statically analyze which part depends on another. This makes it hard to deprecate unused code.
  • Code sharing is not easy enough. There’s a mechanism in Angular which can be used (or abused) but it’s tied to the service/controller/directive model and it comes with overhead.
  • Filesize is hard to control. As the front-end responsibilities grew, so did the filesize. With no obvious code splitting mechanism, our bundle quickly grew larger than a couple of hundred kilobytes.
  • Bower was causing more problems than it was solving. The premise of bower is great, but the implementation was (and maybe is) poor. Conflict resolutions, caching issues and poor Bitbucket support were all reasons for us to explore other options.
  • Testing was not a requirement. It’s incredibly hard to write tests after a feature has landed, from both a budgeting and a motivational point of view. Not having tests makes refactoring incredibly hard, leaving us with code nobody wanted to touch because of the uncertainties about intent.
  • Linting wasn’t either. We had linting in editors installed, but it was the user’s responsibility, and linting was pretty soft (JSHint with only a few basic rules), which meant most PRs had to be shot down because of styling issues.

This, among other reasons, led to the decision to deprecate our client and start a per-view rewrite, which meant we could completely rethink our structure.

Structure

We wanted to have a structure which allowed us to create multiple clients which share code. Here’s what we settled on:

`client` is the folder which contains all our frontend code. Underneath it is a `src` map, which contains both the source and unit tests, and a `test` folder which contains some boilerplate for running unit tests, and the integration tests themselves. In `src` we have our applications (divided by views), and a folder `shared` where we place the components which can be shared across applications. Every component has its own folder and module and consists of an `index.js` file, a `test.js` file, plus optional templates and styling.

Bundling

Given the need for explicit dependencies, the two most viable options for bundling were Browserify and Webpack. We chose the latter because of the (apparent) better support for code splitting & loading, and non-JS assets such as CSS and HTML being a first-class citizen in the Webpack world.

Webpack currently takes care of the following for us:

  • Converting ES6 code with Babel
  • Building a dependency graph and bundling via `import` statements
  • Pulling in SCSS files, analyzing and bundling of referenced assets
  • Importing HTML files and exposing them to the JavaScript component
  • Minifying and deduplicating JS, HTML & CSS
  • Splitting bundles into parts where we define split points and taking care of the lazy loading

The end result is: smaller (initial) files, more reasonable and predictable asset management, and a statically analyzable dependency graph. We can also easily share code across views and applications, and package components for consumption in the older views to ease the transition.

Testing

Testing is now considered part of the feature, instead of after-the-fact. Especially unit testing is a first-class citizen:

  • Every component has (or should have) a test.js which contains the tests for that specific component.
  • We write our tests with Jasmine.
  • Karma takes care of running tests, and monitoring files for changes so it knows when to re-run the test suite.
  • Coverage reports are generated with Istanbul via karma-coverage.

We want tests for three reasons. The first and foremost is making sure and proving it works, but it also makes it easier to refactor, and it can serve as documentation for a feature, which makes it easier for other developers to pick up the work. The simple fact that we have a test suite in place which is constantly being monitored and reported, makes it very easy and rewarding to write tests.

Coverage reports generated by Istanbul

Linting

While we previously had linting, it was through a very small set of JSHint rules, and it was the responsibility of the developer as it was only used via a SublimeText plugin. We still do that now, but we use ESLint, with a pretty strict, project-specific set of rules, and we use a pre-commit git hook to verify no lint errors make it to production (if you want to read more about ESLint, here’s a great article by Dan Abramov).

While we had a few issues with setting this up (mostly performance issues and Webpack’s weird API & documentation), now that it runs, it has made things much easier. Tests have increased stability, code sharing has ramped up our development speed and linting has increased code quality and minimized code review distractions. There are things we need to improve on (integration testing being the number one concern), but we definitely feel more confident about our process every day.

--

--