Battle-tested architecture and file-structure for Angular projects

Alexander Ciesielski
angular.services
Published in
5 min readJan 24, 2021

Over the course of the years I have worked on many Angular projects. Some of them were already on-going and others I had the privilege to create from the start.

In this article I want to share my approach to architecture in an Angular project, which I perfected over the years, which dictates how to structure folders & files and how to properly separate concerns for better readability, maintainability and easier writing of unit tests.

Divide files by business domain (feature)

This is a controversial topic since there are just as many opponents to this rule. To keep this article short and concise I will just focus on why I think this approach fits Angular better.

One advantage is that files that relate to the same feature are close together, meaning when working on a feature you don’t need to scroll long lists of files to find the ones you are looking for (which arguably is a weak argument when working with VS Code’s keyboard shortcut “Ctrl+P” to quickly open files).

Imagine you are writing a reusable DatatableComponent which includes multiple DatatableFilterComponents and these filter components need to be registered in a DatatableFiltersService. Having all components in a /components folder causes visual overlap with other feature’s components.
Additionally, the service is totally invisible at first glance.

For newcomers to the project this is an additional mental effort that needs to be made, since there is no clear distinction.

Having all related files stored in the same folder serves another benefit, if e.g. your manager decided to create a second app with the same datatable logic you have in app 1.
Having to create a Datatable library to be shared between both apps would mean you would have to hand-select the folders from the /components folder, next from the /services folder to copy-paste the feature, instead of just copying the /datatable folder and being done with it.

Keep files close together, divided in their own feature-module folders.

One concern per file

This idea is very well known but needs repeating. Many times I see projects where multiple classes and interfaces are kept in the same file. This decreases visibility of declarations that are “at the end of file” and in general is counter-intuitive and unexpected.

Declare interfaces in separate feature-module.model.ts files.

One component per module

Refactoring applications oftentimes becomes a pain because of having “trashcan modules” (often called shared), which imports lofts of other modules, declare lots of components and pipes, and provide lots of services.

Do yourself (and me) a favor and declare every component in its own module, export that component and then import that module in whatever other module you need.

Create a separate module file for each component.

Separate HTTP calls from business logic

This rule is again mostly useful because of being able to easily copy-paste. Having written one suite of unit tests for an HTTP service it is trivial to copy-and-adapt this suite to a completely new one, thus saving you time twice — (1) because it’s easier for our brains to understand, and (2) because adapting unit tests is much less time-consuming than creating ones from scratch.

Declare HTTP calls in separate feature-model.http-service.ts files.

Smart vs. dumb components

Explaining smart vs. dumb components would drastically increase the length of this article, which is why I will redirect you to this or this explanation.

Really short — the advantages are (1) performance due to being able to use ChangeDetectionStrategy.OnPush, (2) again easier testability, (3) again easier on our brains.

A good example for this might be a (routed) LoginPageComponent — smart, which contains a LoginFormComponent — dumb.

The LoginPageComponent injects services like the router or the AuthService (which contains the actual business logic, like doing HTTP calls).
The LoginFormComponent on the other hand contains only @Input and @Output decorators for its communication with the outside world.

Testing this is trivial, all (routed/smart) components will be tested the same way by mocking external services, then checking if the contained dumb components have their inputs set properly and if an emitted event correctly calls the component’s function. That’s it.

Dumb components are tested easily since its enough to set the input properties on the component, check the template and check the output event emitters for expected objects. Simple!

Break up complex components into smart (inject services with business logic) and dumb (presentational, little-to-no logic) components.

Separate state & business logic from components

After having learned about container & presentational components and knowing that components should preferably contain as little logic as possible then question arises — where is the business logic supposed to be then?

As you may have guessed from previous points — it’s services.

Optimal service & component structure for the authentication module

Similarly to the AuthHTTPService I always recommend using a separate form service contain one or multiple FormGroups, which contain logic only related to modifying and retrieving a form’s value. Not only is this easier to understand for newcomers, as all best practices in this article it let’s us easily write tests for all form-related logic.

The AuthService is the centerpiece of the whole authentication module. I like to call it the public API of the LoginComponent or the whole authentication module itself.

It contains all business-logic related methods and objects, like

  • the login() method, which internally may retrieve the login form’s value and next calls the AuthHTTPService
  • the user$ BehaviorSubject, which is where we put the response from the login me

Usually this approach of having all business-logic related methods and objects defined in a single service is sufficient. More complex apps can benefit from additional separation of the service into a store and query service — which is the topic for another article (hint, it’s Akita).

One more thing

Running into problems when working with Angular?
In need of an Angular expert to strengthen your team?

​Check out angular.services and easily book an online session with one of our experts or hire us long-term.

--

--

Alexander Ciesielski
angular.services

Having started programming at the age of 11 it has been my passion ever since. I’m a fullstack developer with an eye for design and I love building UIs.