An elegant Angular architecture

Zhichuan JIN
Frontend Weekly
Published in
6 min readJan 3, 2021

--

After several years’ design and development of various full-stack applications from scratch, I notice how important it is to build a good architecture for angular applications from the beginning. Good architecture can save us from heavy refactoring when the applications become large and complicated, as we all know that product owners will never stop adding new features in the backlogs :). Therefore, I would like to share this elegant Angular (V2+) architecture to help new developers avoid detours. On the other hand, this architecture works well for big-growing projects as well as for hackathons where a great number of angular developers can work together and produce expected results at the end of the day.

Now, you may ask me, what is an “elegant” architecture?

Here are my criteria:

· A clear and clean structure that allows a new teammate to get involved in our projects more quickly

· Excellent extensibility allowing us to add new features without touching the skeleton

· Good performance without a long loader making users impatient

· Reasonable building time and a small bundle size

· Including a mocking and testing mechanism

· Completed building scripts for all environments

Let’s start with a global overview:

As you can see from the graphs above, there are two types of modules, the eager loaded ones and the lazy loaded ones. So, what are the differences between the two modules? By default, NgModules are all eager loaded, meaning that they are loaded as soon as the application is bootstrapped even if we don’t need them immediately. As a consequence, the loading time of the welcome page can be extremely long. In order to create better user experiences for the customers, the lazy loading technique is strongly recommended to developers. The technique can help developers to reduce the initial bundle size and the loading time. For the implementation, please refer to https://angular.io/guide/lazy-loading-ngmodules.

So, what are those modules? Let’s see one by one together :)

· App Module: the entry point, the default root module for the application

· App Routing Module: the root routing module

· Core Module: the module where we put all singleton services and we will talk about the module in detail later.

· Authorization Module: It’s in charge of the authorization. When the backend denies the users’ access, they will be redirected to the unauthorized page. As you may know, we put the famous “Guard” in this module. If you have never heard about the “Guard”, the following link can help you understand more about it https://angular.io/api/router/CanActivate

· Error Handling Module: it contains all error pages except those already existed in the authorization module. For example, we can put our customized 500 error pages of the connection failure to the backend in the module.

· Shared Module: where we can put all reusable elements. We will also talk about this module in detail later.

· Feature Module & Feature Routing Module: as its name implies, a feature module implements a concrete feature that is composed of several pages, feature services, and a routing module.

Core Module

The core module is designed for singleton services shared by the whole application. As for reusable services, it should be placed in the shared module. On the other hand, for services only used by one feature module, the best place will be in the feature module itself.

So, which services are singletons?

· All interceptor services: for example the http-interceptor service for capturing error responses from the backend

· Loader service: not everyone uses a specific service to control the loader. However, I think it is the best idea ever. A singleton loader service can avoid the display of multiple loaders simultaneously. We can simply pass the service wherever we want!

· Toaster service: it is similar to the loader service

· Configuration service: it reads configuration from the environment file and gives us the right URL of the backend

· Analytics service: you may add this service if you are using tools like google analytics

· Navigation service: for recording users’ navigation histories

Besides, the core module is a very special module because we should guarantee that it is imported only once in the app module. Therefore, we should create a special constructor with the decorators Optional and SkipSelf:

We know those decorators are for dependency injection. Optional means that it is okay if there’s nothing to inject. In other words, the injection will succeed even if the injector named parentModule does not exist. SkipSelf means skipping itself, the constructor should search for other CoreModule than the module itself.

When the first time the CoreModule is imported, the constructor is called with a null parentModule, so the import will succeed. However, if the CoreModule is imported the second time, since there is already a CoreModule, an error will be thrown.

That’s how we detect if the CoreModule is imported into another module. This is what we don’t want to see in the process of the import of CoreModule.

Shared Module

Unlike the core module, the shared module can be imported as many times as we want. In a large application, we have reusable pipes, components, directives, and services. We can reuse them by declaring them in the shared module and importing the shared module either in a feature module or in the root module sometimes.

However, we may think that an element will be used only by one feature module at the beginning of the implementation, so we directly declare it in the feature module. When we want to reuse the element in another feature module later, we will need to move it into the shared module.

What if we want to use the same component in different applications? The solution will be extracting this component to an angular library. As for the subtle widget, I will prefer to do it in React and import via its CDN link.

Best practices

Before we say goodbye, I want to share three small points of best practices.

The first one is to choose the right dependencies. For example, if you can simply use a date format pipe or a small DIY method, there is no need to install moment.js. You can’t imagine how big moment.js is, it will be a disaster for the bundle size!

The second one is to avoid using native DOM or jQuery operations directly in angular codes. As Angular becomes stronger and stronger, there are always equivalent Angular solutions. While implementing a modal, instead of opening or closing it with a jQuery selector, I would rather integrate NgbActiveModal of ng-bootstrap, its API offers us all clean operations https://ng-bootstrap.github.io/#/components/modal/api. If you want to access DOM elements in a component, don’t forget the decorators @ViewChild and @ViewChildren.

Last but not least, good communication with the backend is necessary. I have noticed performance problems after I joined a project in the middle of its development. The loading time of the welcome page displaying the product list is extremely long. Therefore, I decided to analyze the logs in the navigator console. You know what, I was shocked by the size of the responses of the backend API. It fetched all the properties of the products that we don’t need them to be displayed on the welcome page but should be displayed on the product detail page. The solution is simple. In this case, we did two separate endpoints in the backend. The first one contains only principle information of the products on the welcome page; the second one, on the other hand, returns all the information of the product with a given id for the product detail page. Therefore, when the communication is bad, we should ask ourselves, do we fetch too much useless information from the backend?

The end

Thank you for reading the article! I know this article is quite general. If you are interested in a particular part, don’t hesitate to contact me. Besides, in order to improve the architecture, please feel free to write your comments below!

--

--

Zhichuan JIN
Frontend Weekly

A self-motivated and open-minded senior full-stack software engineer worked for SAP, Orange, and Societe Generale CIB.