How to organize and name applications and libraries in an Nx monorepo for immediate team-wide benefits

Jurgen Van de Moere
Product & Engineering at Showpad
9 min readSep 20, 2019

At Showpad, we develop and maintain various Angular applications.

As a leading innovator in sales enablement, it is vitally important for Showpad to provide customers with the best possible experiences to empower sellers and engage buyers.

This offers interesting challenges in terms of pioneering, stability, scalability, consistency and maintenance.

To deal with these challenges, we develop multiple applications in a monorepo environment that uses TypeScript, Angular CLI and Nx’s Angular CLI power-ups.

When developing multiple applications and libraries in a monorepo, it can be surprisingly difficult to organize and name applications and libraries in such a way that all team members can easily find pieces of code and get immediate insights in whether or not the code can be used and/or updated.

On page 66 of the fabulous Enterprise Angular Monorepo Patterns book by Nrwl, we are presented with the following diagram on “where to create new libraries”:

Armed with this diagram, we experimented with different grouping methods until we finally arrived at a convention that we feel comfortable with.

In this article, we share the naming guidelines that we use at Showpad, based on the Nx recommendations, to name our libraries in the Showpad monorepo so that each team member can easily locate libraries and get immediate insights into a library’s responsibilities.

By the end of the article, you will understand:

  • the difference between two fundamental building blocks in an Nx monorepo: applications and libraries
  • how to organize applications
  • how to organize libraries by scope and type so that it becomes immediately clear who can use them and what they contain
  • why organization is so critically important

By sharing our learnings with you, we hope to help you save time and efforts on your journey to create your own guidelines. Or if you want, you can even adapt our guidelines and use this article as your source of documentation.

So let’s get started

For the sake of demonstration, let’s assume we have the following existing applications:

electron-app
web-app
windows-desktop-app
windows-mobile-app

and the following libraries:

auth
charts
dashboard
data-table
icons
post-message
workbox

and that we need to move all applications and libraries into our monorepo.

The first thing that is immediately noticeable is that there is no clear relationship between applications and libraries.

Can the auth library be used in the windows-desktop-app? Can the auth library be used in the windows-mobile-app? Or can the auth library be used in all applications?

How can we organize our applications and libraries inside our monorepo in such a way that we clearly communicate who can consume which libraries?

To be able to answer that question, we must first clearly define and understand the difference between an application and a library.

Applications and libraries

Applications and libraries are two fundamental building blocks in a monorepo.

An application:

  • can be built into a deployable artifact
  • contains a configuration for its build process
  • contains a configuration for runnings its tests
  • can consume code from libraries

A library:

  • contains code that can be consumed by applications or other libraries
  • contains a configuration for runnings its tests
  • can consume code from other libraries

A monorepo can contain multiple applications and multiple libraries.

Now that we know what applications and libraries are, let’s have a look at how we can organize them.

Organizing applications

Applications are organized in the apps directory:

/apps/electron-app
/apps/web-app
/apps/windows-desktop-app
/apps/windows-mobile-app

To create a new monorepo with a first application, we can use the following Angular CLI command:

$ ng new <app-name> 

Once we have a monorepo in place, we can add additional applications by running the following Angular CLI command in the root of the monorepo:

$ ng generate application <app-name>

Each application has its own src directory and can have its own styles, build configuration, TypeScript configuration and unit test configuration:

/apps/electron-app
├── jest.app.config.js
├── src
│ ├── app
│ ├── assets
│ ├── environments
│ ├── favicon.ico
│ ├── index.html
│ ├── main.ts
│ ├── polyfills.ts
│ └── styles.less
├── tsconfig.app.json
└── tsconfig.spec.json

Behind the scenes, Angular CLI provides each application with an entry in the angular.json file, which is located in the monorepo’s root directory, to store the application’s build, lint and test configuration.

Creating and naming applications is pretty trivial because applications run by themselves and are not consumed by other applications or libraries:

/apps/electron-app
/apps/web-app
/apps/windows-desktop-app
/apps/windows-mobile-app

In contrast, libraries can be consumed by both applications and other libraries so organizing and naming libraries requires a bit more attention.

Organizing libraries

Libraries are organized by scope and type in the libs directory:

/libs/<scope-1>/<type-1>-<lib-name>
/libs/<scope-1>/<type-2>-<lib-name>
/libs/<scope-2>/<type-1>-<lib-name>
/libs/<scope-2>/<type-2>-<lib-name>

Library scopes

A library scope is:

  • a logical grouping to organize libraries semantically
  • represented by a directory

The goal of a scope is to provide insights in what part of the logic a library deals with, so that:

  • all team members can immediately see from the scope whether or not they can consume a library in their application or library
  • the maintainers of the library know exactly which applications and libraries can depend on their library and have full awareness of the impact of their changes when they update a library

To name a scope, we use the following rules:

  • If a library is built specifically for use in one application, its scope is the application name e.g. “windows-desktop”, “windows-mobile”.
  • If a library is built specifically for use in all applications and libraries, its scope is “shared”.
  • If a library is built for use in a subset of applications and libraries, its scope is a grouping name e.g. “windows”.

In our example, we would define the following scopes:

  • electron: libraries that can be consumed by the electron-app and by all other libraries in the electron scope
  • shared: libraries that can be consumed by all applications and all libraries in any scope
  • web: libraries that can be consumed by the web-app and all other libraries in the web scope
  • windows: libraries that can be consumed by the windows-desktop-app, windows-mobile-app and by all other libraries in the windows, windows-desktop and windows-mobile scopes
  • windows-desktop: libraries that can be consumed by the windows-desktop-app and by all other libraries in the windows-desktop scope
  • windows-mobile: libraries that can be consumed by the windows-mobile-app and by all other libraries in the windows-mobile scope

Library types

A library type is:

  • a logical way to provide a library name with extra semantic value
  • represented by a library name prefix

The Enterprise Angular Monorepo Patterns book defines and recommends the following types:

  • data-access: contains services and utilities for interacting with back-end systems and all code related to state management
  • feature: contain smart UI (with injected services) for specific business use-cases or pages in an application
  • ui: contains only presentational components (that rely only on inputs and outputs)
  • util: contains common utilities and services used by other libraries and applications

This list of scopes and types provides us with the following possible combinations:

/libs/electron/data-access-<lib-name>
/libs/electron/feature-<lib-name>
/libs/electron/ui-<lib-name>
/libs/electron/util-<lib-name>
/libs/shared/data-access-<lib-name>
/libs/shared/feature-<lib-name>
/libs/shared/ui-<lib-name>
/libs/shared/util-<lib-name>
/libs/web/data-access-<lib-name>
/libs/web/feature-<lib-name>
/libs/web/ui-<lib-name>
/libs/web/util-<lib-name>
/libs/windows/data-access-<lib-name>
/libs/windows/feature-<lib-name>
/libs/windows/ui-<lib-name>
/libs/windows/util-<lib-name>
/libs/windows-desktop/data-access-<lib-name>
/libs/windows-desktop/feature-<lib-name>
/libs/windows-desktop/ui-<lib-name>
/libs/windows-desktop/util-<lib-name>
/libs/windows-mobile/data-access-<lib-name>
/libs/windows-mobile/feature-<lib-name>
/libs/windows-mobile/ui-<lib-name>
/libs/windows-mobile/util-<lib-name>

The result

When we apply all guidelines to the names of the applications and libraries in our example, we end up with the following structure:

# Applications/apps/electron-app
/apps/web-app
/apps/windows-desktop-app
/apps/windows-mobile-app
# -----------------------------------------------------------------
# SCOPE: electron
# Contains libraries that can be consumed by
# - apps: electron-app
# - libs: other libs in /libs/electron
# -----------------------------------------------------------------
/libs/electron/feature-dashboard
/libs/electron/feature-shell
# -----------------------------------------------------------------
# SCOPE: shared
# Contains libraries that can be consumed by
# - apps: all apps
# - libs: all libs
# -----------------------------------------------------------------
/libs/shared/data-access
/libs/shared/ui-dashboard
/libs/shared/ui-charts
/libs/shared/ui-data-table
/libs/shared/util-auth
/libs/shared/util-post-message
/libs/shared/util-workbox
# -----------------------------------------------------------------
# SCOPE: web
# Contains libraries that can be consumed by
# - apps: web-app
# - libs: other libs in /libs/web
# -----------------------------------------------------------------
/libs/web/feature-shell
/libs/web/feature-dashboard
# -----------------------------------------------------------------
# SCOPE: windows
# Contains libraries that can be consumed by
# - apps: windows-desktop-app, windows-mobile-app
# - libs: libs in /libs/windows-desktop and /libs/windows-mobile/
# -----------------------------------------------------------------
/libs/windows/ui-icons# -----------------------------------------------------------------
# SCOPE: windows-desktop
# Contains libraries that can be consumed by
# - apps: windows-desktop-app
# - libs: other libs in /libs/windows-desktop
# -----------------------------------------------------------------
/libs/windows-desktop/feature-dashboard
/libs/windows-desktop/feature-shell
# -----------------------------------------------------------------
# SCOPE: windows-mobile
# Contains libraries that can be consumed by
# - apps: windows-mobile-app
# - libs: other libs in /libs/windows-mobile
# -----------------------------------------------------------------
/libs/windows-mobile/feature-dashboard
/libs/windows-mobile/feature-shell

So why is this useful?

Because from this new structure, we can now easily get insights in who can access which libraries.

For example:

  • the shared/util-auth library can be consumed by all applications and by all libraries
  • the windows/ui-icons library can be consumed by the windows-desktop-app and the windows-mobile-app , but not by any application or library outside of the windows scope such as the web-app or electron-app or web/feature-dashboard library

As a result, when we are working on a piece of code, we know exactly which library we can consume and which library we can not.

In addition, when we are updating a library, we can now safely predict which parts of the monorepo can be affected and which parts cannot.

In a follow-up article, we have an in-depth look at how we can use and configure Nx’s awesome code analysis tool to enforce programmatically that all applications and libraries only consume the libraries that we want them to.

If you want the new article to automatically show up in your Medium homepage or in your Medium digest email, just hit the “Follow” button.

Summary

When developing multiple applications and libraries in a monorepo, it can be surprisingly difficult to name libraries in such a way that all team members can easily find them and know immediately whether or not the library can be consumed.

Organizing libraries by scope and type is a mechanism that helps us conquer that challenge and offers many immediate team-wide benefits.

When creating applications, we can:

  • reuse existing code by consuming libraries from the shared scope or from a scope that our application has access to

When creating libraries, we can:

  • reuse existing code by consuming libraries from the shared scope or from a scope that our library has access to

When consuming libraries in both applications or other libraries, we can:

  • use library scopes to easily navigate through the monorepo and find the library we are looking for
  • use library scopes to learn immediately whether or not we are allowed to consume a library
  • use library types to get insights in what type of logic and code is stored inside a library

When creating and maintaining libraries, we can:

  • use a library scope to express a clear intent and boundary on who is allowed to consume our library and who is not
  • use a library type to express what type of code is allowed to be part of our library and what type of code is not
  • rely on the library scope to safely predict the impact of our changes in applications and other libraries when updating our library

The Enterprise Angular Monorepo Patterns book presents us with a very practical diagram on “where to create new libraries”.

This article builds on that diagram and describes how we apply Nrwl’s recommended guidelines at Showpad.

By sharing our learnings with you, we hope to help you save time and efforts on your journey to create your own guidelines. Or if you want, you can even adapt our guidelines and use this article as your source of documentation.

Happy coding!

Want to join our team and help us build awesome things?
Visit our careers page.

--

--

Jurgen Van de Moere
Product & Engineering at Showpad

Front-end Architect | Principle Engineer @Showpad | Developer Expert @Google