Skip to content

Architecture guidelines for large Angular applications

September 15, 2021Marc-Antoine Laville8 min read

Angular for Large Applications

TLDR;

When coding an Angular app for the first time, this is dangerous to keep old habits and reproduce patterns that worked for other apps like React or Vue. Working in a very large (~100 devs) project, I know that old habits can make code of poor quality.

I propose some guidelines inspired from Angular's recommendations and Clean Code practices that will help your team to write maintanable code and ease newcomer's onboarding!

  • Put the code right where it belongs : use the power of services and pipes to unclog your components.
  • Code in a reactive way your components with services
  • Avoid passing inputs/outputs all around thanks to the Dependency Injection System.
  • Split Services when they become too big. Do the same with modules

Old habits that get you wrong

Those patterns you need to let go of

Angular's intimate relationship with Typescript makes it easy to put data logic everywhere. Therefore it's easy to agree that dumb contains view-printing, and smart contains fetching. It is a sound decision to improve your code readability.

In terms of performance and seperation of duties, Angular proposes an idiomatic way to code. But many of us have been tempted to try famous patterns like Container Components (or smart), or trying to reproduce stateless Components of React and other frameworks.

That being said, Data often plays a central role. Once again, the temptation to try famous patterns like functional components and a Redux data-store is very strong. Indeed, some don't see how to find a robust way to manage their data.

Nevertheless, trying to mimick patterns you learned on other codebases blindly into an Angular application is such a dangerous idea and I want to show you how to make your Angular application maintanable.

Improving readability in consulting companies

I have been working here at Theodo (also in the UK) with teams that encoutered a huge developers turnover. Developers rarely stayed more than one year on the same code base. I even found myself switching teams every 2 months.

Therefore, it is crucial to choose a pattern that will help new developers to quickly switch codebases. That's why I want to introduce you to the right way of thinking your Angular Application Architecture. This approach makes small use of other famous patterns and mainly explains Angular's recommendations.

Guidelines for clean and efficient Angular coding

Use angular-cli

These are the rules you can establish in your team to make you Angular application cleaner. And as a quick rule, I advise you to always use angular-cli when creating new components and services in your app, so you're not tempted to write less.

You are writing presentation code in .ts or .html ? Make a pipe

If the data needs simple mappers, create pipes that you will use in component's template. Indeed those mappers add usually nothing but noise to your code, hide them away !

Don't write functions for data transformation in your component's .ts, and don't write complex functions in your component's html template. Code smells :

Here is an example of a too-intelligent component. It has functions in its template, with logic...

<ul>
  <li *ngFor="let member of team.member">
    {{ isUserFrench(member) ? '🇫🇷' : '🚩' }} Star: {{
    isUserCapableOneOfTheBest(member) }}
  </li>
</ul>

You can put everything in a pipe, a test it seperately !

@Pipe({
  name: 'memberBasicInfoList'
})
export class MemberBasicInfoListPipe implements PipeTransform {
  transform(team: Team): {flag: '🚩' | '🇫🇷', isStar: boolean}[] {
     //calculate flag and isStar
     return {flag, isStar}
  }

Then you have a very readable template !

<ul>
  <li *ngFor="let memberInfo of team | memberBasicInfoList">
    {{ memberInfo.flag }} Star: {{ member.isStar }}
  </li>
</ul>

Make reactive Services that support your components business logic

Services are your intelligent blocks of code. If you wanted to put code in a container component, put it here ! Here are some some rules of thumb :

Wrap all the data fetching in a service, and inject them in the component(s). This service should expose ready to use observables. It should also give methods to send data from components to the service. Same rule applies if a service becomes too big : split it and inject one service into the other service.

Read more about :

Illustration of the reactive pattern

Code in a reactive way with observables as inputs and funtions as outputs

Use observables in your components as if they were already containing the data you want, because that is not the role of component to update the data. Then build public methods in your service to notify it of the last events. Your component should contain very minimum logic.

Think of your component as a puppet of the service. It shows the data the service wants. It tells the service when something happens.

Split your services when they become bigger than 100 lines

Put business logic as soon as possible in services and prepare the data inside of them to be displayed. For example a TeamMember.service that relies of TeamsStore.service and for example another User.service (that can come from somewhere else than TeamsModule)

To split your code, detect parts of code that are heavily linked to each others.

When your base module becomes too big and handles many topics : split into smaller modules

Put common business items in a module, for example "TeamsModule". Split them conviniently with Angular routing as soon as possible. When ?

  • Some parts become more and more independant, and some parts are never displayed together
  • Your module is too big for a new-comer to understand it

Your parent/children modules can communicate through common services. Learn more about when to split modules here : Angular: Understanding Modules and Services

Let's study an example: Breaking down one component into smaller simpler pieces

We talked about writing reactive services, and breaking down big chuncks of code into smaller meaningful one. This example shows a typical example when you have a very big component that needs to be refactored. I often found myself in this exact situation.

InviteTeamMemberComponent(httpClient: HttpClient, errorService: ErrorService, router: Router)
A. Can call the team-backend and POST a new team member
B. Will show an error message (in global snackbar) if failed
C. Will redirect to Team dashboard if successful
D. Creates FormGroup for team member informations (also stores the data and validates it)
E. Displays the data in the template
F. Keeps track of the loading state of the query

That's a lot of responsibilities, huh ?

Let's create a client that can do http-calls, especially the POST (task A.)

TeamMemberClient(httpClient: HttpClient)
A. Can call the team-backend and POST a new team member

and inject it in another service which has the responsibility to manipulate data (tasks B, C and D).

InviteTeamMemberService(teamMemberClient: TeamMemberClient, errorService: ErrorService, router : Router)
B. Will show an error message (in global snackbar) if failed
C. Will redirect to Team dashboard if successful
D. Creates FormGroup for team member informations (also stores the data and validates it)

that we inject inside the initial lighter component that does the two last UI features (tasks E and F)

InviteTeamMemberComponent(inviteTeamMemberService: InviteTeamMemberService)
E. Displays the data in the template
F. Keeps track of the loading state of the query

Avoid Input/Output and prefer injecting specialized services

You can sometimes avoid Input/Output drilling (more specialized article) with either service or content injection, this will look like a simple advice to Redux advocates, but here is the good news : we don't need redux !

Here is an example of input drilling where a 2 level deep button opens a modale upper in the hierarchy, all managed by a smart ancestor component !

Example of input drilling

Valuable code is spread among all the components and useless Input/Output mechanism pollutes the intermediary. On top of that, the ancestor component contains the data, and has the responsibility to update it. Here's what you would prefer :

Solving input drilling with a service

This was an example of refactoring using service. You can also avoid one layer of Input/Output with content projection. In the example above, you could get rid of @Output() edit if you directly inject the button of the action.

What are the advantages of doing so ?

  1. Synchronization within your team : You can still use old patterns, but don't make it a must ! Just communicate on your rules with your team and write coding guidelines that every one of you has read. You'll see tremendous improvement very quickly
  2. Simpler hierarchy : your DOM is not polluted with useless container components. CSS is simpler, and inspecting the application is simpler too ! You can understand it faster ! If other teams use the same coding guidelines, you'll be able to switch teams easily.
  3. Easier onboarding, as a new developer you know where things are by opening a package : services, components and pipes are doing what you expect them to do ! Also, less wrappers means less clicks to get to meaningful code !
Marc-Antoine Laville

Marc-Antoine Laville

Web Developer at Theodo