Declarative Route Path Management in Angular Apps — Even Better Than Best Route Forward

Declarative Route Path Management in Angular Apps — Even Better Than Best Route Forward

When I read Netanel Basal's article — “Best Route Forward — Declarative Route Path Management in Angular Apps”— I wanted to try out the solution to route path management described in the article right away in the apps I work on. The solution in Netanel’s article is intended to help with managing routes in large Angular apps. The idea is great! However, I quickly discovered that the solution does not quite work for the apps that have many feature modules with their own routes — i.e large apps. If these feature modules have their own lazy feature modules with their own routes, a single service class really does not cut it. Let me demonstrate what I mean using a simplified example.

Here is an AppModule with the following routes:

There are two lazy modules for routes “products” and “customers”. The Products module contains a feature module as well. Here are the associated feature route declarations:

Products routes:

EditModule routes:

A class with methods, like in Netanel’s article, works great for a flat route structure:

But what can be done with the routes for the lazy feature module? Below are three naive options that come to mind.

🤔 Naive Option #1

Create methods only at the top-level disregarding the nested nature of routes:

Here’s how it would be used:

This approach has some clear downsides:

  • The methods for a feature module are managed within the same class.
  • The method names are long and repetitive.
  • Each child route explicitly specifies the parent /products path.
  • This will get really ugly for child routes of the edit feature module.

🤔 Naive Option #2

Have the products method return an object to attempt to represent the nested nature of routes:

Now, something like this can be typed:

This feels a bit better, but there are still a few downsides:

  • The methods for a feature module are managed within the same class.
  • The root products route is lost.
  • Each child route explicitly specifies the parent /products path.

🤔 Naive Option #3

Create a separate class for products routes:

This approach also lets the route be used like so:

Now, the ability to manage child routes in separate files is gained, but the ability to use Angular’s dependency injection has been lost! The following downsides still exist:

  • The root products route is lost (could add a method root()?).
  • The explicit use of this.parentPath does not feel DRY.
  • the parentPath needs knowledge of where it is in the hierarchy of lazy feature routes. Otherwise resulting URL will be wrong.

💪 RoutePathBuilder

Long story short, I decided to create a solution that will keep all the benefits of Netanal’s solution and add the features I was looking for:

Original features

  • A single source of truth for each path in the application
  • Strong typings
  • Access to Angular’s dependency injection
  • Use of absolute links (meaning, the generated link is absolute)

New features:

  • Managing routes of feature modules via separate classes
  • Use of property chaining to reflect the nested nature of the routes
  • No explicit use of parentPath in method implementations. Use of relative URL parts for the assembly of the URLs.
  • Flexible return type: to access either a url, a urlTree (useful for RouteGuards), or seamlessly navigate() to the desired route
  • A utility function to simplify the use of the this.route.createUrlTree(commands) method

The @ngspot/route-path-builder library consists of a single abstract class — RoutePathBuilder. Here is how the new library will describe the routes using the hypothetical example from above.

With this setup, inject the AppRoutes anywhere in the app and use it!

The url and urlFromCommands methods return an instance of the AppUrl class. This class has the url and urlTree properties and a navigate() method. With this in mind, here’s how the AppRoutes service can be used:

Here’s how AppRoutes can be used in a route resolver:

The RoutePathBuilder provides a root() method that returns the AppUrl for the root path of a given feature module. For example:

The RoutePathBuilder also exposes two protected properties — router and injector. The router is there as a convenient way to access the router in case it is needed without having to inject an extra service in the component or service. The injector is also there to avoid providing dependencies in the constructor. For example:

Of course, dependencies can also be provided in the constructor, but in that case, Injector needs to be added to the dependencies and super(injector) added the to the body of the constructor.

Notice the use of { providedIn: 'any' } for the services that extend RoutePathBuilder. This means that a separate instance of that service will be created for each lazy feature module of the app. This is important because the injector should be the reference to the injector of that lazy module, not the injector of the root module. This way, accessing a service declared in the lazy feature module will not fail.

I hope you find the @ngspot/route-path-builder library helpful. I wish you happy navigating!

👏 Special thanks to Ana Boca for reviewing, testing, and providing some of the code for this article.

🚀 In Case You Missed It

Follow me on Mediumor Twitterto read more about Angular and JS!

29