Architecture guidelines for large Angular applications
Marc-Antoine Laville8 min read
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 :
- You call functions in your template (See why that’s a bad idea)
- You write a lot of logic in {{ your template }}
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 :
- Tarun Sharma’s Reactive Pattern for data services in angular
- Paul Clavier’s Reactive Programming with rxjs observables
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 !
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 :
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 ?
- 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
- 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.
- 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 !