Scalable Angular project structure and practices

Ash Prosser
5 min readJun 23, 2020

--

Don’t feel like a big read? Head straight to the repo!

One of the most common issues I come across with Angular projects, especially in enterprise with very large projects, is a lack of clear direction on project structure and practices.

It’s no surprise. The documentation for how to build a scalable and maintainable app is very difficult to come by. You end up having to glue many different strategies together without clear guidance on the bigger picture.

In this article I will document my own approach. Like many things in development, this is an opinionated topic. You might find some things here cool, awful, weird or hopefully awesome.

Root Folder Structure

First thing some Angular veterans will notice is that there is no Shared Module. This is because, in my experience, a single shared module will often grow massively in size as an application grows. Components/services/.etc that end up being dumped in there as developers become unsure as to where they should go. Eventually classes become difficult to find and keep track of. This is exactly what we’re trying to avoid.

We keep our Core Module as all Angular applications require an entry point. You will notice in the repo that our CoreModule just holds a single component for exposing the router outlet.

We use Feature Modules to divide up our applications code by feature. In the case of our application, we have three features: CatFacts, Dog Pics and our apps Configuration

Finally we have a Router Module. This will be covered later on but an important detail here is we do not use our feature modules for routes.

Feature Modules

https://angular.io/guide/feature-modules

By dividing our application up by feature we can establish a code base that’s easy to navigate and scales as the business adds additional functionality.

Basic Feature Module breakdown

Our features will be broken down by what they are. Components, interfaces, tokens, services .etc

But what about pages? You may have heard them called something else. Logical components, smart components or containers. The idea of ‘pages’ is coined from Ionic.

Pages

https://blog.angular-university.io/angular-2-smart-components-vs-presentation-components-whats-the-difference-when-to-use-each-and-why/

You will find your application significantly easier to test and maintain if you divide more complex components between those that are just there for the UI.

Pages will typically handle the API, the state or router. Pages will then pass this information down to normal components (Otherwise known as dumb or presentational components)

For an example, you will see in the repo the CatFactsPage requests the cat facts from the API service, this is then passed to the CatFactsList component.

This simplifies these two components with a separation of concerns. The CatFactsList only has to worry about the UI side of things while the CatFactsPage is the middleman between the API and our application.

This also means we can re-use our CatFactsList in other features and from other sources, if needs be.

Module-per-component

https://stackoverflow.com/a/49902078/4112651

Frequently I’ll find projects which have the components piled into a giant shared module.

The question is, if you do this, how do you know what dependencies at a component has? What dependencies can you safely remove? And do you really need to import an entire module of hundreds of a components when you only need one?

You can avoid these problems by having a module per component. You gain a very clear indication of the dependencies, importing the component into other component becomes a breeze and your lazy loaded routers become incredibly efficient.

Speaking of the router…

https://medium.com/@shairez/angular-routing-a-better-pattern-for-large-scale-apps-f2890c952a18

You will notice in the repo that the routing is contained within its own folder and module.
This keeps our routing simple as our folders+modules can reflect the final result of our apps URL structure.

App Initializers

https://dzone.com/articles/how-to-use-the-app-initializer-token-to-hook-into

Startup logic should be kept, if possible, in very simple initializers. By doing this you are essentially ‘tapping-in’ to the angular startup process, before any components are bootstrapped or routes are initialized.

Real-time configuration

We use Azure DevOps pipelines as a CI/CD process. One extremely frustrating problem was having to rebuild the application whenever a configuration needed changing. A good example of this would be an API url changing depending on which environment the app is deployed to.

This is because the environment.ts is compiled into the app at build time. Wouldn’t it be great if we could build our app once, and deploy to any environment? Or change the environments via a simple token replacement?

You can use the Fetch API coupled with Angulars bootstrap, to declare providers on startup. As found here. Storing our configuration as an InjectionToken that can be injected where needed.

This makes testing incredibly easy whereas anyone who has tested angular extensively will know how painful testing environment.ts is.

In your CD process, you can use a token replacement on the json file. This file will be fetched on app startup and injected as a token.

Testing

https://github.com/ngneat/spectator
http://www.natpryce.com/articles/000714.html

Testing in development should be as easy as possible.. else developers will just give up and before you know it you either have a broken test suite or extremely poor coverage.

In the repo you will find that there is a ‘root’ testing folder, for testing utilities that may be useful across the board, and a testing folder for each feature, for utilities specific to that feature.

You’ll find the repo uses the concept of builders. This is inspired by Nat Pryces post here. I think it’s important to have some mechanism for creating test data as this will drastically reduce the maintenance required for your tests if your interfaces ever change.

The repo also uses Spectator. I cannot tell you enough how much this library simplifies Angular testing. Angulars tests also be incredibly difficult to understand for those newer to Angular or developers in completely different fields. Spectator goes some way to simplify Angulars testing to read, and to use.

Closing

Hopefully this article will help someone.. but if you have any questions please do drop them below. I’m much better at answering questions directly than I am writing articles on medium!

Todo

  • Follow up with structuring NGRX state in large applications

--

--

Ash Prosser
Ash Prosser

Written by Ash Prosser

Senior Developer for Animal Friends Insurance. Writing about Angular/C# .NET Core/Node.js