Architecture in Angular projects

One year ago, I published Understanding Angular modules (NgModule). This post was focused on a technical point: scope, to know when to import a NgModule. You should read it first, but it didn’t explain how to organize your own modules.

Recently, I was challenged about architecture in Angular projects. Until now, I was mostly following what is suggested in Angular doc. But against big projects, several flaws appeared and something was wrong.

So I dove into Angular documentation: there are now 12 long pages to explain how NgModules work, including a FAQ. But after reading all this stuff, I was more confused than before. Basic questions like “where is the good place to provide a service?” don’t have a clear answer, there are even sometimes contradictory suggestions.

So I took some time to rethink the whole thing and implement a decent architecture for Angular apps, with these goals in mind:

  • consistency: simplicity (for small apps) and scalability (for big apps),
  • reusability in different projects,
  • optimization (consistent with or without lazy-loading),
  • testability.

A French version of this post is available here.

Angular modules

What is a NgModule?

The purpose of a NgModule is just to group components and/or services which belong together. Nothing more or less.

So you can compare that to a Java package or a PHP / C# namespace.

The only question is: how do you choose to group things together?

Types of Angular modules

There are 3 main types of NgModules you can do:

  • modules of pages,
  • modules of global services,
  • modules of reusable components.

You’ll at least do modules of pages (otherwise your app is just empty). The 2 other types of modules are optionals, but they will come soon if you want to reuse and optimize your code.

Modules of pages

Modules of pages are modules with routing. They are here to separate and organize the different areas of your application. They are loaded only once, either in the AppModule or via lazy-loading.

For example, you could have an AccountModule for the register, login and logout pages; then a HeroesModule for the heroes list and hero details pages; and so on.

These modules contain 3 things:

  • /shared: services and interfaces,
  • /pages: routed components,
  • /components: pure presentation components.

Shared services for pages

To display a page, you need data first. Here come services.

Soon, several pages will need the same service. Thus the shared directory.

But be sure your services for pages are specific to the module, because if you opt for lazy-loading, they will just be available in this particular module (which is good), and not elsewhere in app.

Let’s take back the AccountModule as an example. The account service should just manage communication with the API (which says “yes” or “no” based on user credentials). The user connection status should not be stored here, because it may be not available elsewhere in the app. It will be managed by a module of global services (see below).

Pages: routed components

A page component just injects the service, and uses it to get the data.

You could display the data directly in the component template but you should not: data should be transferred to another component via an attribute.

Each page component is associated to a route.

Presentation components

A presentation component just retrieves the transferred data with the Input decorator, and displays it in the template.

You’ll see this type of components also referred as pure components.

Is this MVx?

On a theoretical level, no. But if you come from the back-end world and it helps you on a practical level, you can compare to it:

  • services would be the Models,
  • presentation components would be the Views,
  • pages components would be the Controllers / Presenters / ViewModels (pick the one you’re used to).

Even it’s not exactly the same concept, the goal is the same : separation of concerns. And why is this important?

  • reusability: presentation components can be reused in different pages,
  • optimizability: change detection of presentation components can be optimized,
  • testability: unit tests are possible on presentation components (just forget tests if you didn’t separate concerns, it will just be a terrible mess).

Summary

Example of a module of pages:

Modules of global services

Modules of global services are modules with services you need through the whole app. As services have generally a global scope, these modules are loaded only once in the AppModule, and then services are accessible everywhere (including in lazy-loaded modules).

You certainly use at least one : the HttpClient module. And you’ll soon need your own. A very common case is an AuthModule to store the user connection status (as this data is needed everywhere in the app) and save the token.

Note: since Angular 6, you don’t need a module anymore for services, as they are auto-providing themselves. But it doesn’t change the architecture described here.

Reusability

Modules of global services are reusable through different projects if you take care to have no specific dependency in them (no UI or app specific code), and if you separate each features in different modules (do not put every service in just one big global module).

As such a module will be used from outside, you should do an entry point, where you export the NgModule, the services and maybe interfaces and injection tokens.

Should I do a CoreModule?

Not necessary. The documentation suggests to do a CoreModule for global services. You can surely group them in a /core/ directory, but as mentioned above, be sure to first separate each feature. You should not put all your global services in just one CoreModule, otherwise you won’t be able to reuse just one feature in another project.

Summary

Example of a module of global services:

Again, the module is not necessary since Angular 6.

Modules of reusable components

Modules of reusable components are modules of UI components you would like to reuse in different projects. As components have a local scope, these modules are imported in each pages modules where you need them.

You certainly use one, like Material, NgBootstrap or PrimeNg. You can do yours too.

How to get the data?

UI components are pure presentation components. So they work exactly the same as in modules of pages (see above): data should come from the Input decorator (and sometimes from <ng-content> in advanced cases).

You should not rely on a service, because services are often specific to a particular app. Why? At least because of the API URL. Providing the data will be the role of pages component. The UI component just retrieves data passed by someone else.

Public and private components

As components are in local scope, do not forget to export them in the NgModule. You just need to export public ones, internal sub components can stay private.

Directives and pipes

An UI module can also be about directives or pipes. Same as components: they need to be exported if they are public.

Private services

Services inside UI modules can be relevant for data manipulation if they contain nothing specific. But then, be sure to provide them in the component, so they have a local/private scope, and certainly not in the NgModule.

Public services

But what if your UI module also needs to provide public services, in relation to the component? It should be avoided as much as possible, but it is relevant in some cases.

You will then provide the public services in the NgModule. But as the module will be loaded several times because of the components scope, it will cause a problem for the services.

You then need an extra code for each public service to prevent them to be loaded several times. It would be too long to explain it here, but it’s a best practice (done in Material for example). Just replace SomeService by the name of your class:

Reusability

Modules of UI components are reusable through different projects. As it will be used from outside, you should do an entry point, where you export the NgModule, the public/exported components (and maybe directives, pipes, public services, interfaces and injection tokens).

Should I do a SharedModule?

No. The documentation suggests to do a SharedModule, to factorize all modules of components inside one module. But I’ll go against the documentation on this one.

Problem is: each module in which you import the SharedModule becomes specific to your app and then will not be reusable in another project.

It’s normal to have to import dependencies each time you need them. And with current tools like auto-imports in VS Code, it‘s not a burden anymore.

But you can surely group your modules of components inside a /ui/ directory (don’t call it /shared/, it will be confusing with services which are shared also).

Summary

Example of a module of reusable UI components:

Conclusion

If you follow those steps:

  • you’ll have a consistent architecture: in small or big apps, with or without lazy-loading,
  • your modules of global services and your modules of reusable components are ready to be packaged as libraries, reusable in other projects,
  • you’ll be able to do unit tests without crying.

Here is an example of a real world architecture:

The goal of this post is also to confront this architecture with the community, ie. you who is reading. So if I missed some use cases, feel free to comment.

By the same author