An architectural approach to better Angular applications#

This article provides an architectural approach to building your Angular applications. This will definitively answer your questions of how to structure your Angular application so the code is readable, testable, and maintainable.

This article is based on a talk that I gave at the 2019 RVA JavaScript Conference in Richmond, Virginia. You can watch it here:

To get the most out of this article you may want to watch the video while you follow along with the text in the article.

All the code examples in the talk and article are in this GitHub repo.

Defining Terms#

We will be looking at Angular Templates, Components, and Services. Specifically, by these terms I mean:

Templates
The HTML part of the component

Components
The TypeScript class part of the component

Services
Injectable classes

Core Principles#

These core principles will be our guide:

  • Templates: Declarative and Dumb
  • Components: Smart and Thin
  • Services: Fat and Happy (Specific)

We will look at examples of what we mean by and how to implement these principles in our Angular code.

Why?#

In my own experience, following these principles has made my code more readable, testable, and maintainable

Specifically, this consistent approach makes our code more readable to others, especially those that are familiar with the approach.

Our code is more testable, because Services are easy to test.

It helps us keep our code DRY (Don't Repeat Yourself). Code in Services is easy to use in multiple components.

It helps us leverage Angular features like the async pipe.

It reduces coupling because we avoid referencing Services directly from our Templates.

OK, let's get started with a discussion of how to approach our Templates.


Templates#

Templates are the Lloyd Christmas of Angular.

Lloyd Christmas and Harry Dunne: please tell me where I can find one of those suits.
Templates should be Declarative and Dumb.

Declarative Templates#

Your Templates should be Declarative and NOT Imperative

Declarative code says WHAT to do

Imperative code says HOW to do it

Declarative Templates define what to show. But, they avoid defining exactly how it is shown.

Avoid complicated logic

Declarative Templates avoid complicated logic such as expressions with && and ||. Move complicated expressions into functions in your Components.

Small and simple

Break your HTML up into smaller and simpler Templates. However, be careful that the splitting doesn't just add complexity.

*ngIf

The top template is the only one that should need to worry about the whether the object actually is loaded. Sub-components should usually assume they have a valid object when they are displayed.

<div *ngIf="selectedTask">
   <rva-task-detail [task]="selectedTask" (done)="done($event)">
   </rva-task-detail>
 
   <rva-task-history [task]="selectedTask">
   </rva-task-history>
</div>

In the example above, note that *ngIf is handled by the parent Template so the rva-task-detail and rva-task-history components can just display without checking to see if selectedTask is valid.

Declarative Template code example

Let's look at some  Template code to see the difference between a declarative versus imperative approach.

In one of my projects I had to create a search filter for processes based on their state. Basically, I needed a set of buttons that you could turn on and off to set some flags for the search criteria.

Now I could have just created a specific component with the five buttons, with each button referencing a field in the JSON object. But like I always say:

Why just engineer something when you can OVER-engineer it?

It reminds me of that hilarious xkcd strip where the engineer designs a generic system to pass the salt:

https://xkcd.com/974/

So, I created a generic component that takes a JSON object with a set of Boolean flags and displays a button for each one.

Imperative Template example (Bad)

The imperative version of the Component looks a little like this:

@Component({
   selector: 'rva-imperative-flags',
   template: `
     <div class="btn-group-toggle">
       <rva-flag-checkbox
         *ngFor="let state of Object.keys(flags)"
         [(flag)]="flags[state]"
         [label]="state"
       >
       </rva-flag-checkbox>
     </div>
   `,
 })
 export class ImperativeFlagsComponent {
   @Input() flags: any;
 
   public Object = Object;
 }

Our component uses *ngFor to display one rva-flag-checkbox for each key in the flags object.

You can see that this imperative version of the Template explicitly says HOW to get the list of keys by using the static Object.keys() function. We also have to add the Object class as a public reference in our Component so the Template can use it.

Declarative Template example (Good)

Let's see if we can improve on this and make our Template declarative:

@Component({
   selector: 'rva-declarative-flags',
   template: `
     <div class="btn-group-toggle">
       <rva-flag-checkbox
         *ngFor="let state of keys(flags)"
         [(flag)]="flags[state]"
         [label]="state"
       >
       </rva-flag-checkbox>
     </div>
   `,
 })
 export class DeclarativeFlagsComponent {
   @Input() flags: any;
 
   public keys(obj: any) {
     return Object.keys(obj);
   }
 }

Now our template is just saying WHAT it wants. It wants the keys to the flags object and it is up to the Component to figure out HOW to get it.

However, maybe we can improve on this a bit more. Because if you're like me, you're probably thinking:

I've never met a piece of code that I didn't want to refactor.

Even more declarative Template example (Better)

We can make our Template even more declarative:

@Component({
   selector: 'rva-declarativer-flags',
   template: `
     <div class="btn-group-toggle">
       <rva-flag-checkbox
         *ngFor="let state of states()"
         [(flag)]="flags[state]"
         [label]="state"
       >
       </rva-flag-checkbox>
     </div>
   `,
})
export class DeclarativerFlagsComponent {
   @Input() flags: any;
 
   public states() {
     return Object.keys(this.flags);
   }
}

Now our Template just directly asks for the states. It doesn't need to know anything about the flags object.

This introduces a principle that we will see later when we discuss Components:

Component functions should be specific.

Dumb Templates#

A Template is Dumb when it doesn't know anything about any other part of the application except for:

  • Its Component
  • Its sub-component Templates

Specifically, Templates should not directly access the Services in the Component. If a Templates needs something from a Service, it should get it from a Component function. In fact, the component services should almost always be private. Let's make that another one of our guiding principles:

Services should almost always be private in the Component constructor.

Let's look at some code examples to see the difference between smart and dumb Templates.

Smart Template example (Bad)

Here is an example of a Template trying to be smart:

@Component({
   selector: 'rva-bad-foo',
   template: `
     <p>{{fooService.getFoo()}}</p>
   `,
})
export class BadFooComponent {
   constructor(
     public fooService: FooService
   ) { }
}

In this example the Template needs to know about both the Service and the correct function to call on FooService. Just imagine how complex this could get if the getFoo() function had parameters.

Also, notice that we had to make FooService public.

Dumb Template example (Good)

In this example our Template is dumb and our Component is smart:

@Component({
   selector: 'rva-good-foo',
   template: `
     <p>{{getFoo()}}</p>
   `,
})
export class GoodFooComponent {
   constructor(
     private fooService: FooService
   ) { }
 
   public getFoo(): string {
     return this.fooService.getFoo();
   }
}

Our dumb Template just knows that it needs to call the getFoo() function. Now we can keep the FooService private.

Furthermore, our dumb component reduces coupling in our application:

Count the arrows

Notice that the smart Template is 50% more tightly coupled than the dumb Template.

Remember:

When it comes to Templates:
dumber is better.

So, we have discussed Templates now let's move on to Components.


Components#

Components are the rock climbers of Angular.

Tommy Caldwell and Kevin Jorgeson climbed the Dawn Wall in Yosemite in 2015
Components should be Thin and Smart.

Or, if you prefer, you can say it like my friend Lars Gyrup Brink Nielsen:

Components should be lean, mean, view controlling machines.
- Lars Gyrup Brink Nielsen

Like me, Lars likes to talk about Angular Architecture. You can check out one of his talks here:

Smart Components#

A smart Component knows and controls exactly what the current state of the Template is.

The Component determines exactly how to interact with the data model or Services based on user activity.

One way we keep our Components smart and our Templates dumb is by having very specific functions in the Component. You should prefer having many small specific functions in your Component rather than a few large generic functions. Let's look at an example:

Generic Component functions

In this example we have a simple Template that displays a Done button and a Remove button. Clicking on one of them should call the appropriate function on the Service.

@Component({
   selector: 'rva-fat-buttons',
   template: `
     <button (click)="handle(action.Done)">Done</button>
     <button (click)="handle(action.Remove)">Remove</button>
   `,
})
export class FatButtonsComponent {
   @Input() task: Task;
   public action = Action;
 
   constructor(
     private taskService: TaskService
   ) { }
 
   handle(action: Action) {
     if (action === Action.Done) {
       this.taskService.completeTask(this.task);
     } else if (action === Action.Remove) {
       this.taskService.removeTask(this.task);
     }
   }
}

OK, this code isn't terrible. It has a little bit of a Flux Reducer feel to it. But, we can probably simplify this code to make it more readable.

Specific Component functions

In this refactor of the code we change our single generic function into two specific functions.

@Component({
   selector: 'rva-thin-buttons',
   template: `
     <button (click)="done()">Done</button>
     <button (click)="remove()">Remove</button>
   `,
})
export class ThinButtonsComponent {
   @Input() task: Task;
 
   constructor(
     private taskService: TaskService
   ) { }
 
   done() {
     this.taskService.completeTask(this.task);
   }
 
   remove() {
     this.taskService.removeTask(this.task);
   }
}

Notice how this simplifies the code not only in our Component but even in the template.

This exemplifies a principal of Component functions:

Component functions should be specific, NOT generic

Thin Components#

So, we discussed the Smart aspect of Components. Now let's discuss what we mean when we say our Components should be Thin.

A Component is Thin when it includes only the code necessary to:

  • Manage the view
  • Invoke Services

Processing code should be moved into Services.

Another tip for keeping your Components thin is the principal:

Components should not inject HttpClient service.

Wrap any Web Service HTTP calls in a back end data Service.

Thin Components: Testable

One reason to move processing code into Services is that they are typically easier to test than Components. A private function in a Component often becomes a public function in a Service making it easy to test.

These public functions are:

  • Easier to test
  • Easier to mock

Smarter = Fatter?

At this point you may be asking, "But, doesn't making my component smarter sometimes make it fatter?"

Well yes, sometimes.

There will be times where you will have to decide between some conflicting principals:

  • Handle it in the Component (smarter component)
  • Handle it in a Service (thinner component)

So how do we decide?

Well the answer is: Like a Boss!

You are probably thinking something like, "OK, cool gif. But what does that even mean?"

When you need to make a decision between doing something in the Component or in the Service, think about your Component as being a smart but rather lazy boss.

The Boss:

  • knows exactly what needs to be done in each situation
  • makes sure it happens
  • delegates actually doing it

So here is another principal for you:

Component: Code that DECIDES
Service: Code that PROCESSES

Let's look at an example of this principal in action:

export class TasksContainerComponent implements OnInit {
   public tasks$: Observable<Task[]>;
   public selectedTask: Task;
 
   constructor(
     private taskService: TaskService,
     private taskMultiService: TaskMultiService,
   ) { }
 
   ngOnInit() {
     if (this.taskMultiService.isMulti()) {
       this.tasks$ = this.taskMultiService.getTasks();
     } else {
       this.tasks$ = this.taskService.getTasks();
     }
   }
 
   public select(task) {
     this.selectedTask = task;
   }
 }

Notice that the code in ngOnInit() is Code that DECIDES and so it belongs here in the Component.

Most developers that I have seen have a tendency to leave too much code in the Component. Since I have been drinking the Kool-Aid longer, I am often tempted to move too much code into the Service.

This reminds me of an excellent quote from Albert Einstein that is applicable to this situation.

"Everything should be made as simple as possible, but no simpler."
- Albert Einstein

We can paraphrase Einstein to give us a principal for our Components:

Components should be as thin as possible, but no thinner.

Let's move on to look at Services.


Services#

Services are the sumo wrestlers of Angular.

Kisenosato is one of only three Sumo Grand Champions in the world today. He is the only one from Japan.
Services should be Fat and Happy.

And, Services are happy when they are very specific.

Fat Services#

By Fat, I mean that business logic goes into your Services. When in doubt whether to put code into a Service or a Component, put it in the Service. Fatten up that Service and starve your Component.

You may even have a Service that only provides functions for a specific Component. That's OK. These classes that encapsulate presentation related logic are often referred to as Presenters. Typically, these Presenter Services should be in the component specific provider. You can locate the files for this Service in the Component folder.

Happy Services#

Services are happy when they can focus on:

  • Doing one thing.
  • Doing it well.

Like a Sumo wrestler, Services can get very big. However in spite of their size, they are really good at only one specific type of fighting.

Happy Services are SOLID#

The theory of SOLID principles was introduced in the paper Design Principles and Design Patterns by Martin. Although, the SOLID acronym was introduced later by Michael Feathers.

Specifically, I want to focus on two of these principles:

Single Responsibility Principle:
A class should have only one single responsibility.

Interface Segregation Principle:
Many client-specific interfaces are better than one general-purpose interface. So don't be afraid of having many small Services.

State-full or Stateless Services?#

I sometimes hear the question:
"Should Services be state-full or be stateless?"

The answer is: Yes

What I mean is that your Services should either be specifically about managing state or be completely stateless.

State-full Services

You should use a State-full Service to manage your application state or any other state for that matter. Some examples are:

  • A Presenter that helps manage the state of a Component
  • An NgRx Store
  • An application state Service

Stateless Services

However, if your Service is a set of functions used for processing, keep it Stateless. Stateless Services handle things like:

  • Processing
  • Data mapping
  • HTTP Calls
  • Etc.

Favor pure functions in your Stateless Services. Pure functions are functions that:

  • Given the same input, always return the same output.
  • Don't produce side effects.

Avoid building state into processing Services like REST API wrappers. For example, let's say our application uses a REST API  to get information from a back-end data store. In our application we should create a Stateless Service that provides functions for us to call the REST API. Now the Service probably needs some information such as the server URL or other connection information. You may be tempted to create an initialize() function or inject some connection object into the service. Don't do it. Keep that Service stateless and pass that connection object into the functions that need it.

Let's look at an example:

State-full Service Example

In this example we have a Service with an initialize function that takes a Server object with a connection URL.

export class StatefullService {
   constructor(
     private http: HttpClient
   ) { }
 
   private server: Server;
 
   public initialize(server: Server) {
     this.server = server;
   }
 
   public getItems(): Observable<Item[]> {
     return this.http.get<Item[]>(this.server.url);
   }
}

Note that we now have to remember to call initialize() first. Also, our Service really should have some error handling code to check if this.server is undefined and let the developer know.

Stateless Service Example

In this example we will keep the Service stateless by requiring the Server connection object to be provided as a parameter.

export class StatelessService {
   constructor(
     private http: HttpClient
   ) { }
 
   public serverFactory(url: string): Server {
     return new Server(url);
   }
 
   public getItems(server: Server): Observable<Item[]> {
     return this.http.get<Item[]>(server.url);
   }
}

You will note that we don't have to remind the developer to invoke the serverFactory() function. The compiler forces it to be called because that's where we get the Server object.

I had this exact situation in some of my code and it made supporting a multi-server system very easy.

The bottom line is that Services should be either:

  • Completely stateless
  • Completely about managing state

Services: Last Words#

Remember these principles that relate to services:

Services should almost always be private in the Component constructor.
Components decide what gets done, but Services actually do it.
Services should do one thing and do it well.

Summary#

Let's wrap this up with a summary of our guiding principles:

Templates#

Declarative and Dumb like Lloyd Christmas

Components#


Smart and Thin like rock climbers

Services
#

Fat but very Specific like sumo wrestlers

If you enjoyed this article, please give the video on YouTube a thumbs up.

Thanks!