DEV Community

Cover image for Component-First Architecture with Angular and Standalone Components
Colum Ferry for Angular

Posted on • Updated on

Component-First Architecture with Angular and Standalone Components

Angular recently had an RFC (Request for Comments) on Standalone Components. It is an effort to make NgModules in Angular optional. We do not want to remove them completely as many Angular apps currently rely on the building blocks that are NgModules.

Manfred Steyer explored what this will mean for the ecosystem moving forward and how we can begin to think about writing our Angular apps in his short blog series:

Declarative Routing

I believe, however, that the best potential architecture we can achieve when Standalone Components are introduced, will be based around Declarative Routing.

Declarative Routing is a concept that we have seen implemented by packages such as react-router. It involves declaring our routes as elements in our component’s template.

With Angular, we do not have an officially supported Declarative Routing solution, however, Brandon Roberts has created a package that implements this concept, called the Angular Component Router.

It allows us to define the Routes through our application in our components, removing the need to configure the RouterModule at multiple layers of our application.

As Standalone Components require us to specify their imports in their @Component decorator, this could get unwieldy. It also means that we’re still relying on NgModules, making it difficult to ever fully remove them from the framework.

Large Standalone Component Imports

Component-First Architecture

However, what if we simply used our component’s template to define the Routes through our application? We could easily have a beautiful, declarative API for our application routing that supports redirects, fallbacks, lazy loading of components (key!), and standard Route Guards!

Declarative Routing

But, we should take this further. Right now, people could define Routes in any component in their application, and figuring out the full routing setup for the application will become extremely painful.

With Standalone Components, we should still slice our application by dedicated features or domains. We’ll create a folder/workspace structure wherein each feature has it’s own dedicated folder/library. At the root of these, there will live a route-entry. This route-entry will contain the routes for this portion of the application. This creates a structure such as:

Folder Structure

We can expect to see a route-entry at the root of each domain/feature we have in our system which will define the routing for that area of the application. Now, every developer will know exactly where to look when they need to find, edit or add routes to the system.

From this, our top-level app routing should only ever point to RouteEntryComonents.

Top Level Routing

Following this pattern with Standalone Components means our components are the driving force of our applications, as they should be.

This is Component-First Architecture.
Component-First Architecture is where our components define and drive the user experience of our application. Anything that impacts the user’s experience should be handled via our components, as it is our components that the user interacts with.

Why should we care about Component-First?

Component-First aims to create an architectural pattern that places Components as the source of truth for your Angular application.

Currently in the Angular ecosystem, NgModules act almost like orchestrators, wiring together your application. It's from the very existence of NgModules where we created the AppModule -> FeatureModules -> SharedModules -> CoreModules architecture.

This architecture is fine. It works. It scales. But is it overkill? Possibly.

While it does introduce a great separation of concerns within your app's structure, CoreModules and SharedModules often become overpopulated and difficult to maintain. SharedModules in particular can become a dumping ground. This often leads to a situation where we need to import the SharedModule into all our FeatureModules, even if we one need 1 thing from it.

With Component-First, our Components decide themselves what they need to perform. They can take Injectables via their constructor and they can import any component, directive or pipe they need to function. This increased level of granularity allows our Components to be hyper-focused on their function, reducing any additional bloat that might end up compiled with the Component.

Components in a Component-First Architecture will be completely tree-shakeable. If they aren't imported or routed to, they won't be included in the final bundle for our applications. Currently, to achieve the same effect with NgModules, we need to follow a pattern known as the SCAM (Single Component Angular Module) Pattern which was popularized by Lars Gyrup Brink Nielsen.

By following the Component-First Architecture Pattern, we also reduce the coupling between our Components and NgModules which paves the way to a truly NgModule-less Angular. We can still keep all the same composability that NgModules offered by simply following some best practices on code-organization; something Angular has us well trained to do already.

If components point to components, our mental mind map of our application becomes simpler. You can traverse the tree of components in your app and build out a pretty comprehensive map of your app, without having to worry about NgModules adding additional dependencies on your components that you may not be expecting. In Component-First, your components dictate their own dependencies. This massively reduces cognitive load, especially for newcomers to the codebase.

Colum Ferry’s Twitter:

Top comments (2)

danield81778548 profile image
Daniel Davis

Have you developed any working examples of this since the release of Angular 15?
In attempting this methodology I receive errors on the [component] approach.

For example, this works:
<route path="/">
<app-home-route-entry *routeComponent></app-home-route-entry>
<route path="/" [exact]="false">
<app-not-found *routeComponent></app-not-found>

This does not
<route path="/" [component]="HomeRouteEntryComponent"></route>
<route path="/" [exact]="false">
<app-not-found *routeComponent></app-not-found>

it fails with the message:

Property 'HomeRouteEntryComponent' does not exist on type 'RouteEntryComponent'.