As a consultant, I normally work with companies between 3-12 months at a time and then I am off to the next gig. Most often, I am hired as a “hands-on” coach, were I am called in for an important and urgent project to make stuff happen within a very tight deadline. This requires that I write a lot of code as well as teaching other developers about the best practices with Angular development. As much as Angular development can seem very different from what we have seen in eg. .NET and Java development, there are some clear parallels that will make everything click very fast once you see them and then Angular development doesn’t seem so foreign anymore. When I am teaching Angular best practices to newer Angular developers with a backend background, I tell them:
What has worked on the backend will also work on the frontend.
Here I am referring to principles of software development that have proven it’s worth over the last couple of decades such as design patterns, clean code best practices, architecture best practices and such. A lot of the patterns such as Redux are originating from backend patterns such as CQRS and event sourcing.
To make it click as fast and possible and make it easy to learn the best practices with Angular development, I like to boil it down to what is now known as “The Ten Commandments of Angular development”. The name is a semi-joke, as these commandments are principles that serve as a Polaris star with simple principles to guide you to do Angular development according to best practices rather than strict rules that should be mindlessly followed 100% of the time, there are of course exceptions as you will see. That means, no 50-page long development documentation, that is most likely going to rotten in some wiki as it is most likely not being read and requires a lot of work to maintain which is resulting in it being deprecated and useless in no time. Instead, keep it short (and catchy!), simple and focused on the essential principles.
For ensuring consistent and high-quality Angular development, I recommend following these ten principles for Angular development:
- Thou shalt separate smart and dumb components
- Thou shalt use abstractions
- Thou shalt have no other gods before Angular best practices/official guidelines
- Remember to type everything to keep it holy
- Thou shalt honor high ROI testing
- Honer thy Reactive architecture
- Thou shalt remember the responsive styling to keep it holy
- Thou shalt not commit without automatic linting and formatting
- Thou shalt make efficient pull requests
- Thou shalt honor performant Angular applications
I have found these guidelines being appropriate for a couple of my clients but some of the points might vary for your specific situation.
Break the component templates down into smaller cohesive components that separate the controllers and presentation responsibility.
Separate components in smart (container) and dumb (presentation) components.
By separating your Angular architecture in smart and dumb components it will ensure segregation of responsibility, consistent level of abstraction and that you develop your app using reusable UI building blocks.
- Have a smart component at the top level of each page containing all the dumb components
- Make sure smart components only delegate using services and dumb components
- Make sure dumb components are
- Encapsulating all the UI logic
- Always use onPush change detection (better performance)
- Don’t inject service
- Only use Input/output for interaction with other components
- Don’t inject services in dumb components
- Don’t do presentation logic in smart components
Read more about this in my post and talk: Refactoring Angular apps.
Use abstractions to decouple the interface from the implementation.
By decoupling the code from vendor libraries that are outside of Angular and use abstractions to reference it so, you can easily change the implementation without needing to update all the usages. The naysayers might be quick to say: “Can we just change the implementation that easily? Isn’t it like when we say use repositories on the backend so we can change the SQL database?”.
In short, it is not the same because:
- SQL database is depending on context outside of the abstraction (repository) such as transaction handling – We are not going to have that problem for eg. HTTP and NgRx abstractions/facades
- SQL database contains persistent state, our Angular app is getting its state freshly hydrated on each refresh
- In the frontend world, frameworks ARE changing yearly, so we have a real need for this
Also, in the frontend landscape frameworks change faster than anything else we have seen on the backend, so we have a real need for being quick on our feet and being able to change implementations without needing to do complete rewrites. I have been on projects were we have changed eg. state management frameworks, HTTP communication and logging frameworks and thanks god, we were using abstractions otherwise we would probably not have been able to.
Another benefit is, by having this coupled away from the UI responsibility, we ensure good segregation of concerns. We keep the UI about the UI and the business logic about the business logic.
- Do encapsulate the trivial UI components through adapter UI components instead of hard coupling the architecture into eg. Bootstrap. This is creating the building blocks for a UI library
- Do create abstractions for repetitive usage of libraries for looser coupling, easier testing and cleaner architecture (eg. state management frameworks, HTTP requests, web sockets) – The more “third party” and the more repetitive the usage, the more you can gain by using abstractions
- Do use abstractions in the UI layer (components) to make the components only be responsible for UI logic and be decoupled from business logic and vendor libraries
- By having the architecture split into smaller libs (and using Nx) this can be inforced so we can eg. for each feature folder have the UI part in a separate library from the business logic part of the feature (a library only exposing the abstraction) thus making it impossible to do the wrong thing (reference implementation details from the outside). Read more here.
- Don’t reference NgRx/state management framework directly in
componentsbut instead, refer them through a
facade(I know the official NgRx documentation shows examples of violating this but demos are not the same as real-life best practices)
- Don’t reference third party frameworks repeatedly in the components (eg. NgRx)
- Don’t do HTTP requests directly from components, instead use abstraction/service
- If you are 100 % sure, that you want to get married to Angular Material, you might decide to hard couple your architecture to directly referencing Angular material everywhere (at own risk). I instead recommend wrapping the UI components and create a UI library for the common UI components, which also acts as an abstraction.
Follow the official style guides and best practices for the frameworks you are using.
By following Angular best practices and official guidelines we are walking on the shoulders of giants and are making it easy to onboard new people that are already familiar with Angular.
- Sometimes you run into edge cases, that require you to bend the rules. In these cases, I normally advise the developers, to discuss it with one of the senior developers and then find a pragmatic step forward.
Angular style guide: https://angular.io/guide/styleguide
Typescript best practices: https://www.typescriptlang.org/docs/handbook/declaration-files/do-s-and-don-ts.html
Note, if any best practices are conflicting between Angular and Typescript best practices, I recommend going with the Angular best practices, as they are closer to what we are working with.
Do type everything and never use
any unless one of the exceptions.
Note: type everything means not using
any or omitting types where types can’t be inferred. If typings can be inferred from the usage it is still typed regardless if it is explicitly defined.
Typing provides the cheapest and fastest error checking mechanism, that can catch errors at build time which could otherwise give runtime problems. If you omit types or use any, you are making development harder as you allow room for mistakes and you are begging for runtime problems as you don’t have an automatic way of ensuring you follow the correct interface.
- Do type EVERYTHING, DTOs (ideally autogenerate from the BE swagger interface using Swagger codegen). I recommended using the
TSLintrule and integrate it into the development flow using Git hooks
- Do enable Typescript strict compiler option to enforce being explicit about null values
- Don’t be lazy and skip typings. You will be paying for it with slower development and more runtime errors. Especially with DTOs, as it will save the other developers from a lot of time and potential mistakes looking up the API documentation all the time as they develop
- Sometimes in tests, you might want to cast to any or partial so you don’t need to declare a whole mock object
Focus on implementing automatic testing in the areas of the app, that gives the highest return of investment. Tests are not equal citizens.
Read more about my testing guidelines here.
Picture by Kent C. Dodds
All automatic tests are not equal. Some will yield a much higher return of investment and by focusing on these we make sure that our time writing Angular tests are spent in the best possible way.
In the end, we want our tests to give automatic proof that our apps are still working on every change if the tests can’t efficiently do that, we might as well don’t write it and get the proof from manual testing.
- Do focus on testing:
- Services (NgRx effects, services, business logic) – unit test with 100 % code coverage
- Pure functions (helpers and pipes) – unit test with 100% code coverage
- Smart components – Integration tests for “happy paths”
- Do focus on creating an end-to-end test suite that covers the top 5 use cases before expanding the suite to more
- Do make sure the e2e tests are retrying commands and expects so they don’t randomly break because of timing problems. For this reason, I recommend using Cypress for E2E testing as all of this is built-in.
- Don’t waste your time writing unit tests for dumb components that are not going to be a good proof anyways. If you do find the tests valuable, I recommend doing it as snapshot tests.
- Don’t try to do e2e testing for every feature as you will be drowning in the maintenance of the test suit and you won’t be focusing on the highest ROI tests. For this reason, focus on mastering the top 5 use cases before expanding the test suite.
Do separate your read from writes. All reads should come from an observable stream hooked into the view using the async pipe (unless exceptions).
- Having a reactive architecture setup enables for better scaling, performance and clean architecture, as you are not mixing read and writes, and you have a clear distinguishment between reads and side effects that can be developed independently
- By using a reactive architecture you are developing your apps regardless if it is sync or async. That means you will not be dealing with timing as with imperative programming where eg. properties need to be set before you can access their values
- Also, by using the async pipe in the templates we are making sure to automatically trigger unsubscribe when the component gets destroyed
- Using the async pipe in templates is simpler than doing subscription
- Use the async pipe in templates
- Always use the observable translations and don’t do “instant” translations with
- Don’t call subscribe in the components (see exceptions) and instead rely on async pipe
- Avoid using the “tap” operator in Observable streams and instead pipe everything down to the async pipe in the views.
- Why? When doing tap we are transitioning from reactive to imperative programming and enabling room for leaking state, timing dependence and breaking the reactive architecture and one-way data flow
- Subscribe: For reactive forms and route subscription you will need to call subscribe
- Tap: Sometimes you will need to call tap eg. to open a modal, but never use it to map state to a variable as that will break the reactive architecture
Use a UI and a layout library for easier, faster and more consistent styling with less code.
Using these tools will make styling easier, faster and more consistent compared to if you were to write it all as pure SCSS/CSS every time. It also makes sure, your application is using a foundation to make UI component development easy and responsive on different screen sizes.
- Do use a UI library like Angular material / Ionic / create your own design system if you have enough dependent projects to ensure you have a lot of UI building blocks at your disposal
- Do use a layout framework like bootstrap grid, Flexlayout or build your own grid system in your design system, to easily set up a responsive skeleton on every page containing defined breakpoints for stacking content
- Don’t do it all with your own CSS/SCSS as it will be slow, inconsistent and hard to maintain
- If you have enough different projects, there might be a business case for building your own design system instead of using an already available UI library like Angular material.
Don’t waste your time by discussing the styling rules over and over in the pull requests. Just automate it and focus on the essentials = providing the best user experience.
- Do automate enforcement of code style and formatting using TSLint, Stylelint, and Prettier
- Do use Git hooks to automatically integrate the formatting and linting as part of the workflow
- Don’t discuss the code style and formatting rules in the pull requests – A lot of nitpicks in pull requests might be a smell that something is not clearly standardized or (even better) automated
Keep pull requests efficient for the fastest development and the best code quality.
A lot of time is spent in the pull requests and it can become a bottleneck and provide little value if done wrong.
Normally, at the beginning of a coaching contract with a client, I tell them to create a value stream map to get an overview of the flow from concept to production, measuring the lead time and identifying bottlenecks. One of the most common bottlenecks I see is tasks spending too much time in review because of an inefficient way of doing pull requests.
Provide a clear description
Do: Provide a clear description that explains the highlights of the change so the reviewers know what to look for
Don’t: Write nothing, just the commit messages or some description, that is not explicitly stating the highlights of the change
Do review with a nonjudgemental attitude:
- Do Ask questions instead of giving commands
- Give commands to the author
- Be judgemental before you know why something is done – ask questions instead if somethings seems odd as there maybe is a reason for it
Do call in for a pair review/programming if the pull request has over 20 comments
To not let a pull request be open for too long be proactive and switch to a high bandwidth communication media when over 20 comments in the pull requests (the exact threshold is up to you, take which seems reasonable).
Once you get together, you can handle the change as either a pair review or even as pair programming for faster feedback loops.
This is one of the biggest improvements you can do, which makes the difference of a PR being open for weeks vs. minutes.
It will be too slow to process a lot of comments and discussions asynchronous, so instead get together and solve it on a call or at the desk in minutes.
- Call in for a pair review/programming if the pull request has more than 20 comments. That goes both for the author and the reviewer
- Let a pull request draw on for weeks with a lot of comments – be proactive and get together either physically or over a skype call if it gets over 20 comments
To ensure we optimize for good performance when we develop apps, there are some principles for optimizing load time and run time performance.
This is not performance tuning. Even with following these guidelines performance problems can occur and in this scenario, I recommend looking into my Angular performance tuning posts.
Load time performance
Load time performance is affecting the time it takes for the user to load and interact with the app.
We want our apps to load fast for a good user experience. For public-facing sites that are optimizing for conversions or other important metrics, I recommend looking further into optimizing load time performance if these principles aren’t adequate.
- Use lazy loading for all feature modules
- Do use webpack-bundle-analyzer to identify your biggest third party libraries and look for tree shakable or more lightweight alternatives, more on this here
- Set bundle budgets to watch the bundle size continuously. What bundle size should cause errors? Use this tool to find the bundle threshold you can “afford”.
- Load everything in one bundle – Use code splitting
- Avoid big es5 libraries (if possible) and instead, look for smaller alternatives (or just copy-paste what you need from Github if applicable)
- The initial route should not be lazy-loaded
- Sometimes you need a big es5 library and there are no good alternatives
Run time performance
Run time performance is affecting the usability of the app and how efficient the user can use the app.
We want the run time of our apps to be as efficient as fast as possible for a good user experience.
- Do use
OnPushon all dumb components while having your architecture broken down in a lot of small components as sensible
OnPushchange detection the default when generating components by setting it in
angular.jsonschematics like here
- Do set data in the template using observables + async pipe, pure pipes, pass down to dumb component/directive or (worst case) map to a state property)
- Don’t skip
OnPushchange detection. If you have problems with change detection after this change, you are most likely not following a reactive architecture with immutable updates (with
OnPushto trigger change detection on input change, you need to pass a new reference)
- Avoid method binding in templates (instead, use observables + async pipe, pure pipes, pass down to dumb component/directive or (worst case) map to a state property).
The commandments here are what I have experienced to be helpful for the projects, I have been involved in. Your situation could be different, so you might want to adjust some of the commandments for your development guidelines. Nevertheless having these guidelines defined in a simple and straightforward format will do a lot for more consistent and efficient Angular development. I recommend, you create such a list of commandments that fits your situation (or use this blog post) and reference it from your “getting started” wiki, so new developers will get on track quickly and you get consensus on how to develop Angular apps.
Personally, the result for implementing these guidelines has been faster, more consistent, fewer comments in pull requests, better lead time and better code quality with Angular development as these guidelines were able to cover the most common scenarios (80/20 principle).
If you are interested in learning more about Angular best practices, in a guided and interactive way centered around your current project (including diving further into these principles), I recommend you apply for my Angular Architect Accelerator course to see if you are a good fit for advancing your career as an Angular architect in a highly skilled mastermind.