DEV Community

Victor Tihomirov
Victor Tihomirov

Posted on

Common mistakes that backend programmers make in Angular

Recently, I found myself in charge of upgrading our Angular application from version 12 to 15. In my current role, which I've held for just over a year, most of my focus has been on the backend, and I hadn't even touched the frontend apps until now. I've got to be honest; this upgrade process turned out to be a tougher nut to crack than I initially thought. It's not a knock on the Angular team's Angular Update Guide, though; they did a solid job. The real challenge came from the fact that we had a bunch of custom-made components that didn't quite follow the best practices playbook.

To provide a bit more context, our entire team primarily comprises .NET developers who possess some familiarity with HTML, CSS, and basic JavaScript. The rationale behind this choice stemmed from the belief that transitioning to Angular would be smoother for backend developers, possibly owing to the familiarity with the MVC (Model-View-Controller) pattern. At the time of this decision, Angular was also considered the go-to standard choice for frontend development.

Although I did manage to pull it off eventually, here's what tripped me up along the way and some insights into areas where improvements could be made. Here are the key points I'd like to highlight:

1. ESLint

While reviewing the codebase, I couldn't help but notice numerous inconsistencies and common mistakes that would have been easily caught by ESLint. After setting up and configuring it with recommended rules for Angular, TypeScript, and RxJS, I ran the linter. To my surprise, it uncovered over 5000 linting issues, with approximately 2000 of them being automatically fixable.

Linting meme

However, my primary goal wasn't to fix all these linting errors, as I couldn't guarantee that doing so wouldn't introduce unforeseen issues. Instead, I chose to install husky and set up linting and automatic fixing exclusively for committed files. In cases where automatic fixing wasn't feasible, I encouraged the developers to follow the "boy scout rule" and address the errors they encountered. As I delved into these improvements, I also took the opportunity to seamlessly integrate and configure prettier alongside ESLint. After all, who enjoys scrolling through diffs or pull requests with only formatting changes? It just makes the code review process smoother and more pleasant for everyone involved.

2. TypeScript

any meme

I found myself inspecting the codebase in sheer horror as I discovered that practically everything was marked as any. The most significant advantage of TypeScript seems to have been overlooked.

Picture writing C# code where everything is either an anonymous type or simply of type object – it's akin to a maintenance nightmare.

This is one of the factors that made me hesitate to automatically fix all the linting issues and approach the upgrade with caution.

3. Unit tests

testing meme

This brings me to the second reason why I hesitated to automatically fix all the linting issues. There are numerous factors contributing to the absence of unit tests in our app, ranging from time constraints and knowledge gaps to a perceived low value, lack of a testing culture, and motivation. I won't delve too deep into each of these reasons, but I can say that the lack of unit tests significantly undermined my confidence in the app's stability following the upgrade. As it turned out, my concerns were justified, as various issues did arise and required subsequent fixes.

4. RxJS: observables and higher-order observables

observables meme

I've noticed that our codebase includes a fair share of Promises, async/await, and RxJS usage. Let's establish one thing: RxJS not only covers all the functionality of Promises and async/await but does so with superior flexibility and maintainability. One of the standout features of RxJS is its inherent support for cancellation and resource cleanup. Promises and async/await don't provide these capabilities out of the box; you'd often need to resort to workarounds or third-party libraries to achieve similar functionality.

I've also come across a common mistake in the codebase where developers tend to nest subscriptions. This practice can introduce a lot of problems, including memory leaks, unpredictable behavior, challenges in error handling, code that becomes increasingly complex and hard to maintain, and a loss of control over the execution flow.

Adding to the mix, I also stumbled upon Promises and async/await nested within RxJS subscriptions. This not only heightened the complexity of the code but also made it significantly harder to understand and read. What's more, there's the looming threat of potential deadlocks because RxJS is tailored to function seamlessly with non-blocking, asynchronous operations, and introducing async/await or Promises into the mix can disrupt this delicate balance.

To steer clear of all these potential issues, I strongly recommend sticking to observables and higher-order observables as the primary means of chaining or flattening them.

5. Composition over inheritance

composition over inheritance meme

This one is probably my favorite. Angular favors composition over inheritance and it's easy to get caught up in the inheritance trap.

Let me illustrate this with an example. Imagine the business comes to me and says, "Hey, we need two forms for our new app." And I'm like, "Sure thing, here they are."

export class ContactComponent {
  errors: string[] = [];
  formData: FormGroup = new FormGroup()

  displayErrors() {}

  submitForm() {}
}

export class RegistrationComponent {
  errors: string[] = [];
  formData: FormGroup = new FormGroup();

  displayErrors() {}

  submitForm() {}
}
Enter fullscreen mode Exit fullscreen mode

Later on, they throw in a request for a new login form. That's when I begin to notice some code repetition. So, I decide to follow the DRY principle and create a base class to handle all this shared functionality.

export class BaseFormComponent {
  errors: string[] = [];
  formData: FormGroup = new FormGroup();

  displayErrors() {}

  submitForm() {}
}

export class ContactComponent extends BaseFormComponent {}
export class RegistrationComponent extends BaseFormComponent {}
export class LoginComponent extends BaseFormComponent {}
Enter fullscreen mode Exit fullscreen mode

Everything seems to be shaping up nicely. I've gone ahead and refactored the code, successfully eliminating those pesky code duplications.

Now, here comes the business again, this time requesting a rating widget to be added. "No worries," I think, "I've got this covered."

export class RatingComponent extends BaseFormComponent {}
Enter fullscreen mode Exit fullscreen mode

Somewhere in the back of my mind, I can't help but think that the rating form doesn't need any validations. But hey, for the sake of consistency, I'll stick to using that trusty base class. Who knows, even if it doesn't need those features right now, it might come in handy down the road, right? Better safe than sorry.

And then, the business strikes once more, this time requesting a new email notification feature. They want users to receive a thank-you email after every contact, registration, or rating submission.

This seems like a straightforward task. I'll just inject an SMTPClient into the base class, and then I can easily share the email notification logic with all its subclasses.

export class BaseFormComponent {
  errors: string[] = [];
  formData: FormGroup = new FormGroup();

  constructor(private smtpClient: SMTPClient) {}

  displayErrors() {}

  submitForm() {}
}

export class ContactComponent extends BaseFormComponent {
  constructor(private smtpClient: SMTPClient) {
    super(smtpClient);
  }
}
export class RegistrationComponent extends BaseFormComponent {
  constructor(private smtpClient: SMTPClient) {
    super(smtpClient);
  }
}
export class LoginComponent extends BaseFormComponent {
  constructor(private smtpClient: SMTPClient) {
    super(smtpClient);
  }
}

export class RatingComponent extends BaseFormComponent {
  constructor(private smtpClient: SMTPClient) {
    super(smtpClient);
  }
}
Enter fullscreen mode Exit fullscreen mode

After going through my changes, something just doesn't sit right. Why on earth should my LoginComponent have to deal with injecting the SMTPClient? This is a significant code smell, no doubt about it.

In a nutshell, here's what I've managed to accomplish so far:

  • Created code that's less flexible and harder to maintain.
  • Introduced tight coupling between components.
  • Achieved a delightful level of code duplication.
  • Made writing tests feel like an Olympic event.
  • And let's not forget the introduction of the notorious fragile base class problem.

Quite the list, isn't it? 😅

Sadly, this is precisely the situation I'm grappling with in the codebase I'm currently working on.

To address these issues effectively, we need to shift our focus to composition instead. Remember the is-a vs has-a?

  • A RatingComponent has an SMTPClientService
  • A LoginComponent has a ValidationEngineService
  • A RegistrationComponent has a ValidationEngineService and an SMTPClientService

By using services in this manner, we can craft reusable code that's not only easier to maintain but also decoupled. Plus, writing unit tests becomes a breeze. This approach offers a fresh perspective and a cleaner path forward.


That's a wrap for my rant. Time to go brew some coffee. If any of you have questions or want more examples, feel free to ask in the comments – I'm here to help and answer them all! ☕😊

If you liked my article and would like to buy me a coffee, you can do it here.

Top comments (12)

Collapse
 
fnh profile image
Fabian Holzer • Edited

One thing I've seen from more than one person (all of which had an extensive Java background) is a mistaken understanding of what it means to do type assertions, especially at system boundaries. Often they tend to model the response from some REST endpoint with a class and write methods and then consume some JSON with the HttpClient, asserting it conforms to said class, and wonder why the JSON which they actually got is not serialized into an object of the class and the method calls yield a runtime errors (but, but, but the compiler said was correct!!!1!). Well, just like every marine is a rifleman first, every TS dev is a JS dev first.

Collapse
 
vixero profile image
Victor Tihomirov

That's a good one 😄

Collapse
 
diegorapoport profile image
Diego Rapoport

Oh, I got you. My problem is that our codebase is in Angular 6. Yeah, and a lot of mixed dependencies with non well separated components that also generates UI inconsistencies. All that to wait for a 1+ hour build. Beyond that it was built on top of JHipster and the company intends to keep it.
It'll be a hell of an extensive work but, TBH, I'm pretty willing to do it.

Collapse
 
vixero profile image
Victor Tihomirov

Ouch, that is painful 😵 And how is it possible to make it a 1+ hour build? 😄

I think the biggest pain points will be upgrading to angular 13 and 16.

Angular 9 introduced the Ivy compiler and in Angular 13 it deprecated the View Engine, so everything is Ivy now.

In Angular 16, they deprectaed ngcc and now all the 3rd party libraries, that are not built with Ivy, are not compatible anymore. That's the reason I stopped at 15, because there are 3 npm packages that we still need to figure out what to do with them.

But overall, I enjoyed it more than I hated it. It was a great learning experience.

Collapse
 
diegorapoport profile image
Diego Rapoport

I believe we'll start all over from the begining with angular 16. But this time thinking about components as they should be planned and designed, and trying to use all of the new stuff angular brings to our benefits.

Yeah, 1+ hour build sounds like a crazy thing but the application is very large and is very far from being optimized in any level. There's a lot of code duplication, which I'll be sooooo glad to get rid off.

I'm also focusing on the learning experience and that's why I'm so eager to start this huge update.

Thread Thread
 
vixero profile image
Victor Tihomirov

Starting over might be indeed a good idea. You can check out my previous article on structuring the app. But if it's a big project, maybe Nx is a better fit.

I am also fond of the container/presenter pattern in Angular, as it makes reading and understanding the data flow much smoother.

Collapse
 
fnh profile image
Fabian Holzer • Edited

What fresh hell of a third-party library is able to hold you hostage 10 major versions behind? Since these libs must be both valuable, stable and more or less abandoned, have you considered to fork or inline them? I found the transition to Ivy rather smooth. But frankly, with that setup, I would run ng new and copy & adjust the app file by file.

Collapse
 
diegorapoport profile image
Diego Rapoport

Actually it wasn't me. When I got hired they said I would help them to update Angular 'cause they got stuck in there like forever. It still works so I guess they took the "if it's working, don't touch it" too seriously lol.

Thread Thread
 
vixero profile image
Victor Tihomirov

"if it's working, don't touch it"

Up to the moment they either get hacked or audited.

Collapse
 
ahryman40k profile image
Ahryman40k

It looks like we are working at the same place :)
Different company, different app, same problems!

Collapse
 
vixero profile image
Victor Tihomirov

😄

Collapse
 
cesard profile image
César

With the last one I would argue that you're breaking other principles related to OOP rather than just not following composition over inheritance: you're breaking the SRP (Single Responsibility Principle) as you're giving too many responsibilities to the base class, when it shouldn't.
Your solution is the correct one for this case, but even if you'd have other behavior that should be inherited (and not only a dependency as your example) you could create another abstract «derived» class from the base class that extends it and that could be inherited by the final component. There'sre multiple ways to work things around and they are very dependant on the context. Inheritance has its place and can and should be used, but always with the correct criterion.

Good article.
Cheers.