How to set up an Nx-style monorepo workspace with the Angular CLI: Part 1
In this step-by-step tutorial, we set up an Nx-style monorepo workspace with the Angular CLI. In this first part, we create an application project, an end-to-end test project, and a feature shell library.
Lars Gyrup Brink Nielsen
Tech Writer, Tech Speaker, OSS Contributor, Microsoft MVP. Frontend Architect at Systemate in Denmark. Passionate about software architecture, testing, and reactive programming.
This tutorial is part of the Angular Architectural Patterns series.
The Nx toolchain by Nrwl helps us work in a so-called workspace which is a monorepo that can manage multiple applications, workspace libraries, and package libraries.
You might not be able to convince your team or manager to buy in to the Nx toolchain. If that's the case, you're in luck. In this tutorial, we'll walk through how to set up an Nx-style workspace using the Angular CLI rather than the Nx CLI. We'll use a custom Node.js tool to generate application and workspace library projects.
We'll implement enough of the Nrwl Airlines example to demonstrate how to work in an Angular CLI workspace with multiple applications, each with multiple platforms. The application domains have a shared feature. The two different platforms within a domain share the exact same feature set and routing by using a feature shell library for orchestration and to serve as an entry point to the application project.
We'll end up with the project folder structure shown in the following figure. This includes 4 application projects, 4 end-to-end test suites, and 12 workspace libraries.
First we'll use the Angular schematics and a few command line tools to generate the projects as per our specifications, but later we'll automate those operations using a custom command line tool called generate-project.
This tutorial will span over 5 parts. In this first part, we're going to create the Angular CLI monorepo workspace, generate the booking desktop application project, its end-to-end test project and the booking feature shell workspace library.
First, we'll generate a new Angular CLI workspace called nrwl-airlines. I assume that you have the Angular CLI installed globally. Use the ng new command.
The --strict flag sets some strict configuration options for the TypeScript compiler.
Clearing the --create-application switch prevents an application project from being generated right away. This is important since we want to use an Nx-style file and folder structure for our projects, including separate folders for applications and workspace libraries.
Our blank workspace is generated with this file and folder structure.
We'll use the json package to edit JSON configurations in our workspace. Install it as a development dependency.
Let's try it out by enabling strict template type checking introduced by Ivy. Run these commands.
In our TypeScript configuration, we now have these Angular compiler options.
Most of our projects will be workspace libraries. We'll set the default project folder to the libs folder which will be created shortly. Do this by running this ng config command.
Booking desktop application
We'll start out by generating the desktop web application for the booking domain.
First, we'll use the built-in Angular application generator schematic with these options.
Next, we'll split the project folder and its configuration in angular.json into two projects – one for the application and one for the end-to-end tests. Run the following commands.
Finally, we'll configure the builders and architect targets for our two booking desktop projects as seen here.
Some of the commands are just housekeeping after splitting project folder and workspace configuration entry. Some mimic the configuration we get when using the Nx CLI with the Nrwl schematics for Angular.
After going through these steps, the file structure of our apps directory is as shown in the previous figure.
As seen in the listing above, the booking-desktop application configuration in angular.json looks like a default configuration except we removed the e2e architect target.
The listing above shows the booking-desktop-e2e project configuration which has the e2e and lint architect targets.
Try them out by running the following commands.
Centralise Karma configuration
Since we're splitting our monorepo up into many projects that each have separate test builders, they're going to need separate Karma configurations. Since they will mostly be the same, we'll create a common Karma configuration file in the workspace root folder as seen in this listing.
We'll replace the desktop application project's Karma configuration with the one shown in the following listing.
Make sure the application's unit tests still work by running the following command.
Booking feature shell library
With the first application project set up, we're ready to create the feature shell library for the booking applications.
We're going to use the command line utility rimraf to remove some of the files that are created when using Angular's library schematic. The reason for this is that the Angular library schematic generates a package library for publishing on a package registry such as NPM.
When using a workspace, we're often creating workspace libraries that might be specific to a single application or might be shared between multiple applications. However, there's often no need to package, version and publish them. This is one of the benefits of using a monorepo.
As seen in the previous listing, we start out by setting the parent folder of the library project, we're going to generate.
Now we use the Angular library schematic to generate a library project with this command.
With the Angular library generator schematic, we get the following file and folder structure.
We're going to get rid of the files ng-package.json, package.json, and tsconfig.lib.prod.json as we want this to be a workspace library, not a package library.
We're also going to remove the service and its test suite as well as rename the Angular module to follow Nx naming conventions.
Use the commands above to rename the workspace library project. We passed feature-shell as the name parameter to the library schematic since that's what we wanted to call the folder since it's already nested in the libs/booking folder.
This way, the file paths used in the library project configuration are correct.
Run the command above to remove the build architect target since we're not going to build this library separately – instead, it'll be built as part of the booking applications.
We'll leave the test and lint architect targets.
We're going to apply linter configurations that are set when using Nrwl's Angular schematics. Run the commands above.
As mentioned earlier, we're going to delete unnecessary files with the following commands.
We generate the feature shell Angular module with a conventional name and export it as shown in the following listing.
Let's also add a test suite for the feature shell Angular module with the content of this listing.
We'll also add a shell component.
We add the router module to its component test suite.
Now we change the content of the booking feature shell Angular module.
The shell component is the entry point component of our layout and features. Every additional route is added to the children array of the shell component route.
Additional routes will be rendered by the router outlet in the shell component template.
The Angular library schematic set up the path mapping in the following listing.
That path mapping was meant to use a package library after it had been built and output to the dist folder.
We don't have a build architect target for the library anymore. Remember that monorepos use workspace libraries straight from their source code and build them as part of an application bundle.
The listing above shows how to set up a path mapping aliased with an NPM scope we pick, namely @nrwl-airlines. The mapping points to the library's public API barrel file, index.ts as seen in the following listing.
Now applications are able to import the feature shell library by using the @nrwl-airlines/booking/feature-shell path.
Remember to replace the library's Karma configuration with the following one.
Use the following commands to run the linter and the unit test suite make sure we've set everything up right.
The following listing shows the generated file and folders structure of the booking feature shell library.
The booking feature shell library has the two architect targets test and lint as shown in the following listing.
Now that we've generated and configured the feature shell library project, let's import the feature shell module in the booking desktop application.
In the initial booking feature shell, we prepared the root route configuration with the shell component in the top level route. Later, we'll add feature routes, configuration, and initialisation.
The previous listing shows how we eagerly load the booking feature shell module which is alright, since it'll in turn lazy load feature routes.
Remember to put a router outlet in the booking desktop application's root component as seen in the listing above.
Make the corresponding changes in the app component test suite.
Also update the end-to-end tests. We match the title to 'booking-mobile' and update the selector to booking-root h1.
Lint and test our application and end-to-end projects ot make sure everything works.
Now run ng run booking-desktop:serve and navigate to http://localhost:4200 in your browser to see our application. It should look like the following screenshot.
At this point, our project folder structure looks like the following figure.
We started by creating a blank workspace without any application or library projects, using the Angular CLI.
To learn how to use the json command line tool to modify JSON-based configuration files, we enabled strict template type-checking.
The first project we created was the booking desktop application. After generating it using the official Angular application project schematic, we extracted the end-to-end test suite and its configuration into a separate project with lint and e2e architect targets.
We centralised Karma configuration by having a base configuration in the root directory of our workspace. The booking desktop application project extends it by setting a unique path for its test coverage reports.
Next, we created our first workspace library, the booking feature shell library. We used the official Angular library schematic to generate our starting point. However, this schematic is meant to scaffold a package library to be published on a package registry such as NPM.
Workspace libraries are only used internally within a monorepo. They can be shared between multiple projects and they don't require versioning since their source code lives with the rest of the projects in the workspace.
We set up an entry point Angular module with routing for the entry point Angular component, the shell component which itself will have child routes for the rest of the application that are dynamically rendered by its router outlet.
To enable the application to import the booking feature shell library, we set up TypeScript path mappings. Then we added a router outlet to the app component and imported the booking feature shell module to connect these two pieces.
As you know, it's important to maintain our codebase as it evolves. We updated the unit test and end-to-end test suites to keep it in sync with the changes.
Finally, we linted the whole code base and ran the unit test and end-to-end test suites.
That's enough work for one day.
In Part 2, we'll automate a lot of the manual work we did today, using a custom generate project tool. We'll use it to generate the shared and booking data access libraries, then add NgRx Store and Effects root and feature state to them. Finally, we'll add the NgRx Store DevTools and NgRx schematics, register the data access libraries in the feature shell library and extract a shared environments workspace library to be able to configure data access the way we want.