architechtural lessons learned building angular projects

Angular Architecture: Best Practices

Introduction 

So you’ve read the “Angular.io” getting started guide, taken the now famous “Tour of Heroes tutorial”, and learnt all about the magic of Angular CLI. Great! you’re on your way to getting started with building your new Angular application. 

Like any new software development effort though, once you figure out what you want to build, you’ll start to formulate questions like, what should the project structure look like? What features should the application have? How will those features interact with each other? How will data flow through the application?  These questions, in my experience, can prove to be quite intimidating for seasoned developers new to the Angular platform. 

While it’s true that there is no single right answer to these questions, there are suggested approaches and many hard lessons that have proven to be very useful in building large Angular applications. In this article, I will discuss a few of these approaches, share my experience, and give recommendations on how to architect Angular applications.

It is assumed that the reader has some familiarity with the “basics” of Angular and therefore the details of the more introductory topics will be skimmed over. 

Let’s get started! 

Project layout and Structure. 

Mono-Repo 

To start a new Angular project with Angular CLI you run the “ng new” command which generates a new application that will have the default CLI project structure, and includes configuration files in the new project folder, a root application module, and a root component in the app/src folder of the project. Prior to Angular version 6, the generated application and all of its sub folders and files were considered to be a single project. All generated components thereafter, services, pipes etc., got added to this structure and were considered to be part of that new project. Starting in Angular version 6 however, Angular introduced the concept of a workspace. On the surface, the project structure you generate for a new Angular 6 and beyond application, looks very similar to that which was generated in Angular 5.x. You may be wondering, what then, is the difference between a 5.x project and a 6.0 and beyond workspace? 

Well, the difference is that the workspace is more than just that a single project. When Angular 6 generates a workspace, it also creates a default application that looks very similar to a 5.x project. However, the workspace is now a container for several projects and libraries that all share the same configuration files. The new workspace in principle is what is referred to as a “Mono-Repo”. There are several benefits to working with the mono-repo structure as opposed to a simple project. The mono-repo can essentially house a suite of similar applications that share the same dependencies, libraries, components and styles. It makes for easier dependency management across those applications, as well as unified tooling, consistency, and simplified cross project changes and updates. 

But how do you activate the power of the “mono-repo”? After generating your application with the “ng new” command, you can use the “ng generate application” command to generate a new project in the repository. That newly generated project is a completely separate application and is run independently of the default application in the mono-repo. When a new application is generated, Angular adds a “projects” folder to the structure and it also adds an entry for the new project in the “Angular.json” configuration file under projects. 

best practices for building angular projects
Figure 1 Mono-Repo structure and configuration

All new generated projects and libraries get listed under this section and can be configured separately from each other. When generating components, services, etc. for this new project, you run the following command “ng generate component test-component --project=new-project”. This essentially tells Angular to create a new component called test-component in the project named “new-project”. 

Creating a new library is very similar to creating a new application in the mono-repo. Instead of using the term application in the “ng generate” CLI command you use the term library. Angular will also add the new library under the “projects” folder in the structure and add the library to the Angular.json configuration file under “projects”. 

This may seem a little elaborate for a simple project, and you might be right about that. You can certainly use the default generated application and structure to build out your app. However, if you work in a team that may want to reuse components and entire feature sets, you will find this approach to development very useful. Moreover, the libraries you build can now be built and packaged as node modules and installed via NPM install. This makes for better code management and reusability. I therefore recommend that this pattern be considered for larger projects. 

Modules and Folders 

It’s difficult to talk about Angular modules without discussing folder structure. By default when a module is generated, CLI also creates a folder for that module along with the “module.ts” and “module.spec.ts” files. But what modules should you generate and where should they be generated? Before we answer this question let us talk about another principle that we will reference throughout the rest of this article, the principle of separation of concerns. 

The separation of concerns principle is a key one in software development and Angular embraces it wholly by design. Angular in fact is very opinionated about how to construct and separate out the distinct parts of an application. The gist of this principle is that your code should be developed and grouped in such a way that allows each part or group to have “single” responsibility. 

By design Angular modules group components, services, and pipes that work together to provide a “specific feature set” for the application. They also provide an execution context and scope for elements that share a similar concern. 

As an example, one of the most common features of an ecommerce application is the ability to do a checkout of items from a shopping cart. This functionality in an Angular application should be encapsulated into a module which can then be reused throughout the application wherever the application allows the user to do a checkout. 

Now to the question of what modules to create. Modules should generally be created to represent a specific feature-set in your application. The feature modules that you build will then naturally make up your folder structure. 

In addition to those feature modules however, in the projects I develop I also add a few standard modules. These I find to be very helpful in organizing the smaller pieces of my application. It’s not important what these modules are called but rather what they contain. I usually create a module called “core-components” to house all of my customized wrapped components, like textboxes, selects, etc. I use the term “wrapped components” to mean, plain html components or components from a specific library, wrapped and customized in an Angular component. This may not be necessary in many cases but if your aim is to make your application look different through customization, this is an approach you may find yourself wanting to take. I also create a module called “shared” to house common things that will be used throughout the application, like custom pipes and utility classes. For projects that utilize the mono-repo structure I create these modules as libraries. Libraries have the advantage that they are made accessible to all of the projects in the repo, which means that a library can be defined once and used across different modules in a specific application or even across the different applications in the mono-repo.  

Abstracting your application into layers 

Even though Angular is very opinionated and does a lot for you by default, as it relates to having single responsibility code, it’s not immediately obvious how to further abstract your code into layers or even how to set up the plumbing to accommodate layering of your application. Why even have layers? By organizing your application into layers, you further adhere to the separation of concerns principle of software development. Layering also makes it easy to understand what each part of your application should be doing, and therefore it helps when thinking about how to organize your code. 

There are several layering paradigms in use today but the one that closely matches the way I organize my code is the MVC (Model View Controller) paradigm. The model layer essentially represents the data layer of your application. Your typescript interfaces, your data-access services, and your state management. 

The controller layer is the layer that acts as the mediator between the view or presentation layer and data layer. It contains the business logic that makes sense of the data and dictates the processes that are performed on that data. This layer will contain your smart components and helper services. 

The view or presentation layer should only be responsible for displaying data and capturing the actions that users perform on that data. This layer will contain your dumb or presentation components and custom data pipes. But how does all this work with Angular? 

Let’s talk about that now. 

In Angular, it is important to note that there isn’t a clear physical separation of the layers. For instance, you won’t find all model elements in a folder called model or the controller elements in a folder called controller, rather the different elements of each layer are wrapped up in the different modules and components of the application that all work together to form a cohesive whole. 

The Data Layer 

Common Interfaces 

The data layer is where you would create and define all your typescript interfaces and group them within specific namespaces. This is done to clearly distinguish between interfaces that can possibly have the same names. There are two ways that this can be done. 

  1. By defining exportable interfaces, for example:
    export namespace Inventory {
    
     
    
                export interface products {
    
                            name: string;
    
                            type: string;
    
                            description: string;     
    
                }
    
    }
     
  2. By creating typescript declaration files, for example:  
    namespace Inventory {
    
     
    
                 interface products {
    
                            name: string;
    
                            type: string;
    
                            description: string;     
    
                }
    
    }
  3.  

Notice in the declaration file example the export keyword is omitted. With declaration files, your namespaces and interfaces will be accessible throughout the application without having to explicitly import them into your components. Declaration files are saved with a .d.ts extension as opposed to simply a .ts extension. To learn more about declaration files please reference the following link: 

https://www.typescriptlang.org/docs/handbook/declaration-files/introduction.html 

Each of the interface definitions should represent a single domain element that your application will consume and use, or an object representation that groups a set of related fields. These interfaces are usually created in a folder called “common-interfaces” in the root of the workspace. They can all be included in a single common-interfaces.ts file  or they can be saved in several files and stored in the common-interfaces folder. 

Data aware services 

Data aware services are services that injects the Angular HttpClient service to make use of the http verbs (Get, Post, Put, Patch, Delete). These services are responsible for fetching and updating data from your back end. Mostly these services would be provided in the root application module so that they are made available throughout the entire application. If however, your application specific data set should only be accessed via a specific feature module, then the service should be provided in that feature module. Data aware services are usually created in a folder called services in the “src/app” path in each project. 

Angular provides an HttpInterceptor interface that proves very useful for intercepting http requests and transforming them as necessary before they are sent out, or for transforming the returned response event stream. Implementing this interface exposes the intercept method that enables the request transformation. A typical example when this would be used is when there is a need to inject a baseURL prefix for all http requests made by the application or for setting a security token in the request header before the request is sent out. This allows for request manipulation in a single place in the application. Typically, in my projects, I would set the different baseURL paths that the project will use in different environment files and have the interceptor service read and inject those baseURLs for each respective environment

To learn more about the HttpInterceptor interface and how to use it, please follow the link below: 

https://Angular.io/api/common/http/HttpInterceptor 

State Management 

An application will typically have several things all happening at the same time that need to be tracked. These include routing, UI changes, object selection from a collection, the persistent state of an object, etc. Managing all of these things in an application can quickly become very complicated. While this can be achieved by writing manual services to keep track of the different pieces of your application that are changing, I would strongly recommend considering a state management library to manage state in your application. For this, my recommendation would be the NgRx library. 

Ngrx is a framework for building reactive applications in Angular. It provides state management, entity collection management, router bindings, code generation, and a set of tools to make managing state much easier. 

State management is huge topic by itself and will be addressed in a later blog post

See the following links for more details about NgRx and state management: 

https://ngrx.io 

https://blog.nrwl.io/managing-state-in-Angular-applications-22b75ef5625f 

The Controller Layer 

Smart Components 

Smart components are essentially components that act as containers for other components. They can themselves contain other smart components as well as dumb components. Smart components have some visibility into the “interfaces” that are exposed from the data layer of your application. Based on the design of your data layer, these components inject facades from your store (NgRx store), the store service itself, or the data aware services. They dispatch actions that are sent to the data layer through these services to perform some operation that either results in the retrieval, storage, or manipulation of the data. If you were to imagine the component tree of your application, smart components would be found higher up in that tree. Smart components more often than not would also be route components, which are simply components that are included in your routes configuration file. 

The purpose of these components in your application would be to mediate between your data layer and your view layer. They fetch and process data, and then pass that data to the view layer to be displayed. 

Helper Services 

These are services that you create to perform some specific functionality either for a specific feature module or application wide. An example of a helper service would be a service that is responsible for providing a standard configuration file for a charting function for some charting library, like Google charts or Highcharts. Helper services perform some utility function. 

The View or Presentation layer 

Dumb Components 

Dumb components are essentially those components that are found at the end of the component tree. Their sole responsibility is to present data to and listen for user action events. These components usually expose inputs to allow for data to be passed in to them, and outputs so that they can communicate events or actions to their parent components. 

While dumb components seem to have the easiest jobs in the component hierarchy, they can be tricky to manage. By default, Angular runs its change detection for components by using its “CheckAlways” strategy (ChangeDetectionStrategy.Default). This means that whenever something changes in any part of the application - even if that change has absolutely nothing to do with this component, change detection is run on all components. Although the effect of this is not felt because Angular is so fast, this can be a relatively expensive operation. 

It is recommended that dumb components use the OnPush change detection strategy (ChangeDetectionStrategy.OnPush). The OnPush strategy only looks at the inputs of a component to determine if change detection should be run. This can be a bridge and a trap at the same time. The trap with this is that when working with OnPush immutable objects must be used for inputs. Change detection will not fire for a dumb component if the object passed into its input was a mutation of a previously passed in object. The input object must have a different reference for change detection to fire when using the OnPush strategy. If, however, the “async” pipe is used in the template when passing in data to a dumb component, change detection recognizes when a new event is fired from the observable in the template subscribed to via the “async” pipe. Since “OnPush” change detection looks for immutable objects, I would caution against the use of primitive inputs (boolean, number, string). 

Concluding Remarks 

When building large Angular applications, it is very important for you to understand how you will architect the solution that you are building. A well architected application easily allows for scalability, effective data management and efficiency in your applications. The following are recommendations for architecting your next Angular project. 

  • Consider using the Mono-Repo style of development when building large applications and working with multiple development teams.
  • Think about how you will modularize your application into specific feature-sets and build your modules to encapsulate those features.
  • Follow the separation of concerns principles.
  • Create abstraction layers in your application; they help to define what each part of the application should be doing and help with code organization.
  • Create your interfaces so that they can be used globally throughout your application.
  • When necessary, use state management libraries to manage state in your application. I would recommend using NgRx or Nrwl.
  • Create smart components in your controller layer.
  • Use dumb components for your view or presentation layer.
  • Pay attention to the change detection strategy used in your components.

We Are Hiring!

Come work with our award winning team! We are looking for mid to senior level developers for positions in both Hampton Roads and Richmond VA. Check out our careers page and send us your resume!

Let's Talk About Your Project.

We are a full-service IT and digital marketing firm. We believe that successful projects are the result of working collaboratively and transparently with our clients. Are you looking for a better user experience for your website or application? Need an experienced database architect or business analyst? Let’s talk!

Let's find what you're looking for