Implicit Libraries with Nx: Lightweight Angular Architectures by Convention

The build solution Nx has been helping to build large projects and monorepos for years.NX DAEMON It supports Angular and React as well as several Node.js-based frameworks out of the box. A plugin concept also allows other frameworks such as Vue.js to be integrated.

Nx accelerates build processes through caching and parallelization and allows restricting access between program parts to enforce loose coupling. Both is usually done at the library level, which can be visualized with a dependency graph:

Nx Dependency Graph

Since libraries in Nx are used not only to create reusable code but also to structure the entire solution, an Nx-based monorepo usually has a large number of libraries. Each library in turn has a large number of configuration files:

Even though Nx generates these files, developers often find them to be overhead that distracts from the actual work. This is precisely the criticism that implicit libraries target. The idea comes from Angular GDE and Nx Champion Younes Jaaidi, who described it in detail in a blog post. Also, he offers a step by step guide here. To get rid of the configuration files, he derives the configuration of the individual libraries using conventions.

In this article I will explore this idea and show it in the context of an architecture we quite often use. The example used, which is based on the ideas in the above mentioned blog post, can be found in my GitHub account. As currently, there is no npm package implementing the idea of implicit libraries, I also had to take over source files from the original example linked here with some modifications. Younes thinks about publishing a library after getting some initial feedback so that using implicit libraries will become easier.

Architecture Matrix

Large Nx projects are often subdivided both vertically and horizontally:

Architecture matrix

The vertical subdivision results in application areas, e.g. subdomains. The horizontal subdivision describes technical layers. Depending on the project, other dimensions are also conceivable, e.g. a subdivision into server-side and client-side source code.

This approach makes the code more structured and reduces discussions about where to store or find certain parts of the application. In addition, architectural rules can now be introduced, such as that each layer only has access to the layers below it. Another rule could specify that a subdomain can only use its own libraries and those from the shared area. A linting rule included with Nx can ensure such restrictions.

Each intersection point in the matrix shown above corresponds to a separate library in Nx:

Architecture matrix shown in folder structure

Implicit Libraries with Project Crystal

To prevent every library from receiving the large number of configuration files mentioned above, the idea of implicit libraries uses an Nx plugin that derives the configurations of the libraries. This is made possible by the so-called Project Crystal. This is an innovation in Nx that allows project configurations to be described programmatically with a graph.

Plugins can be placed either in npm packages or directly in the Nx project. The example discussed here uses the latter option and places the plugin under tools/plugins/implicit-libs/src/index.ts. This file exports a tuple createNodesV2, which Nx uses to determine the implicit libraries and their configurations:

export const createNodesV2: CreateNodesV2 = [
    'libs/**/index.ts',
    async (indexPathList, _, { workspaceRoot }): Promise<CreateNodesResultV2> => {

        […]        

    }
];

The first entry defines a glob. The plugin recognizes each match as an entry point into a library. Libraries are therefore folders under libs that have an index.ts.

The second entry defines a function. Nx passes the determined entry points to the first parameter indexPathList. The variable workspaceRoot refers to the root directory of the entire Nx workspace.

The task of this function is to create the configurations of the individual libraries and to store them in the form of a CreateNodesResultV2 object. The linked example configures eslint and vitest for each library to run unit tests.

It also derives a categorization for the libraries from the folder structure. This categorization reflects the position in the architecture matrix (see image above). For example, a feature library in the domain Tickets is assigned the categories type:feature and scope:tickets .

The categories determined in this way are the basis for the above-mentioned architectural rules that the linter enforces. They can be found in the eslint configuration file in the root directory of the Nx workspace:

rules: {
  '@nx/enforce-module-boundaries': [
    'error',
    {
      enforceBuildableLibDependency: true,
      allow: [],
      depConstraints: [
        {
          sourceTag: 'scope:checkin',
          onlyDependOnLibsWithTags: [
            'scope:checkin',
            'scope:shared'
          ]
        },
        {
          sourceTag: 'scope:luggage',
          onlyDependOnLibsWithTags: [
            'scope:luggage',
            'scope:shared'
          ]
        },
        {
          sourceTag: 'scope:tickets',
          onlyDependOnLibsWithTags: [
            'scope:tickets',
            'scope:shared'
          ]
        },
        {
          sourceTag: 'type:feature',
          onlyDependOnLibsWithTags: [
            'type:feature',
            'type:ui',
            'type:domain',
            'type:util'
          ]
        […]
    ]
  }
}
[…]

To make Nx pick up the plugin, reference it in the nx.json file, which is also located in the root directory of the monorepo:

"plugins": [
    "./tools/plugins/implicit-libs/src/index.ts"
]

In addition to the plugin, the shown example also contains a generator that sets up path mappings for all implicit libraries:

nx g @demo/implicit-libs:update-tsconfig-paths

For the above mentioned reason, this generator was also taken from the original example. The generated mappings allow access to the individual libraries via logical names:

import { TicketsService } from '@demo/tickets-data' ;

Angular Architecture Workshop (online, interactive, advanced)

Become an expert for enterprise-scale and maintainable Angular applications with our Angular Architecture workshop!

All Details (English Workshop) | All Details (German Workshop)

Disabling daemon and cache

Nx exposes all project configurations and information about dependencies between projects through a daemon. It also places the result of individual build tasks in a build cache.

While these measures significantly improve the performance of Nx, they can be a problem when developing plugins. To prevent plugin results from ending up in the cache during development, it is recommended to disable both mechanisms. This can be done, for example , by setting the environment variables NX_DAEMON and NX_CACHE is set to false . On Windows, the following instructions can be used:

set NX_DAEMON=false
set NX_CACHE=false

Implicit Libraries in Action

To create an implicit library, simply set up a corresponding folder under libs and add an index.ts:

Structure of an implicit library

To check whether Nx recognizes the implicit library, it is a good idea to create a dependency graph:

ng graph

Alternatively, you can tell Nx to print the names of all libraries to the console:

nx show projects

To find out how the plugin has configured each library, the following statement can be used:

nx show project tickets feature booking

Nx then generates a page describing the derived configuration:

View derived configuration

Here, for example, you can see that the library tickets-feature-booking has been given the categories (tags) type:feature and scope:tickets and supports linting and unit tests. Accordingly, the following commands can be called:

nx lint tickets-feature-booking
nx test shared-ui-common

The categorization is incorporated into the linting rules to enforce the architectural specifications. For example, if you try to access the Luggage domain from the Ticketing domain, you will receive an error message:

Detecting an access violation

No Silver Bullet

As everything, Implicit Libraries are no silver bullet. In the above mentioned blog article, Younes points out that most Nx built-in plugins use the project.json file to configure targets, so you'll need to adapt your Implicit Libraries plugin accordingly; however, it can be challenging to control all tool options through target configurations, such as setting the cache directory for Vitest, which can cause issues in certain modes.

Conclusion and Outlook

Implicit libraries, whose configurations are derived by an Nx plugin using conventions, make working with Nx much easier. To create a new library, you only need to set up a folder with an index.ts . This is made possible by Project Crystal, which allows Nx projects to be described in the form of a graph.

Similar to our linter Sheriff, Implicit libraries bring another flavor implementing the general idea of slicing large-scale in loosly-coupled sub-domains that can be implemented by different teams. Eventuelly, there will be a npm package implementing this idea, so that people can use it more easily.

eBook: Micro Frontends and Moduliths with Angular

Learn how to build enterprise-scale Angular applications which are maintainable in the long run

✓ 20 chapters
✓ source code examples
✓ PDF, epub (Android and iOS) and mobi (Kindle)

Get it for free!