Enterprise Angular Mono Repo Patterns
Enterprise Angular Mono Repo Patterns
ANGULAR
MONOREPO
PATTERNS
Nitin Vericherla
Enterprise Angular
Monorepo Patterns
Nrwl Technologies
Brought to you by
Since 2016, our team members (distributed throughout the USA, Canada, UK and Europe) have
worked with global enterprises to provide remote consulting, training and software engineering. Our
modern open-source development tools and practices enable software teams to achieve better
collaboration, accomplish their immediate delivery needs and build long-term team capabilities and
success. To learn more visit https://nrwl.io/services and tell us about your project.
Introduction
How this book is organized
In Part 1 we begin by looking at development in a monorepo. We cover some basics about Nx and
look at how to get started with Nx.
In Part 2 we look at libraries in depth: how to organize them, name them, combine them and other
techniques to aid in code reuse and modularization.
In Part 3 we look at how to enforce quality and consistency across the monorepo with the tools
built into Nx.
In Part 4 we look at how we can help to make more intelligent builds and to use the Nx tools locally
and in a CI pipeline.
In Part 5 we look at some common development challenges when working in a monorepo with
many teams with different release schedules.
In Appendix A we look at how to interact with Nx using other tools (Angular Console).
In Appendix B we look at all of the commands that you can use in Nx and refer to specific sections
in the book where you can find more information on the command.
In Appendix C we look at some common how-tos and illustrate the decision trees for common
questions.
Formatting
Asides are formatted like this and indicate information that might add
some context to the topic being described.
1
An example reference application
As a reference that we can use throughout the book, let’s consider the fictional Nrwl Airways. There
are three teams in this organization:
• Booking: The team works on allowing the user to book a flight to a destination.
• Check-in: The team works on allowing the user to use on-line check-in for a flight that they’ve
booked.
• Seatmap: The team works on allowing the user to pick a seat on a flight graphically.
There are four (4) applications that are deployed separately: check-in (desktop and mobile) and
booking (desktop and mobile). The end user is served one of these applications based on the URL
they visit (booking.nrwl-airlines.com or check-in.nrwl-airlines.com) and the browser metadata that
is sent with the request (to send them a desktop or mobile experience).
2
Part 1: Getting Started
Why a monorepo?
Many large organizations and business units within an organization are opting to develop all of
their code (including numerous front-end applications as well as back-end applications) inside of a
single repository. There are a few goals with this approach:
Increase visibility
Teams within an organization or business unit are often unaware of changes that other teams are
making, and these have a large impact during integration. A lot of time can be saved if integration
issues are discovered as soon as code is checked in. We discuss some strategies to deal with the
changes in Part 5 of the book.
Furthermore, API contracts are easily accessible in a monorepo and can be used directly by both
the front-end and the back-end. Types can be generated from the contracts and consumed by
both the front-end and back-end to ensure that errors are caught at compile-time rather than the
more expensive integration-time.
Developers might alternatively use npm link or ways to simulate the dependency management for
local development, but this is also cumbersome to set up and use: there might be a lot of
dependencies that we need to set up in this way and we have to remember to remove the links
when we are done.
Working in a monorepo allows you to refer to the dependency directly (using workspace-relative
paths in the case of Nx). The code is also available to the developer to work on directly, and we
discuss ways to integrate changes to shared code in Part 5 of the book.
3
Why Nx?
Large organizations encounter some issues that one might not find in smaller teams:
• While ten (10) developers can reach a consensus on best practices by chatting over lunch, five
hundred (500) developers cannot. You have to establish best practices, team standards, and use
tools to promote them.
• With three (3) projects developers know what needs to be retested after making a change. With
thirty (30) projects, however, this is no longer a simple process. Informal team rules to manage
change no longer work with large teams and multi-team, multi-project efforts. You have to rely
on the automated CI process instead.
In other words, small organizations can often get by with informal ad-hoc processes, whereas large
organizations cannot. Large organizations must rely on tooling to enable that, and Nx provides this
tooling. It includes the following:
• It contains schematics to help with code generation in the projects to ensure consistency.
• It contains tools to help with enforcing linting rules and code formatting.
• It allows you to visualize dependencies between apps and libraries within the repository
• It allows commands to be executed against code changes: any uncommitted
changes, comparison between two specific git commits, comparison against another branch,
etc. These commands can test, lint, etc. only the affected files, saving us a lot of time by
avoiding files that were not affected by our changes.
Let’s look at some of the basic building blocks of Nx: workspaces, apps and libs.
Nx Basics
What is a workspace?
A workspace is a folder created using Nx. The folder consists of a single git repository, with folders
for apps (applications) and libs (libraries); along with some scaffolding to help with building,
linting, and testing.
What is an app?
An app produces a binary. It contains the minimal amount of code required to package many libs to
create an artifact that is deployed.
The app defines how to build the artifacts that are shipped to the user. If we have two separate
targets (say desktop and mobile), we might have two separate apps.
4
In our reference example the four (4) applications are organized as below:
apps/
booking/
booking-desktop/ <--- application
booking-mobile/ <--- application
check-in/
check-in-desktop/ <--- application
check-in-mobile/ <--- application
Apps are meant only to organize other libs into a deployable artifact - there is not a lot of code
present in the applications outside of the module file and maybe a some basic routing. All of the
application’s code is organized into libs.
What is a lib?
A lib is a set of files packaged together that is consumed by apps. Libs are similar to node modules
or nuget packages. They can be published to NPM or bundled with a deployed application as-is.
The purpose of having libs is to partition your code into smaller units that are easier to maintain
and promote code reuse. With Angular CLI 6.x, libraries can be used within applications in the /
apps folder or even be bundled and deployed to NPM as a stand-alone package.
Some libraries are used only by a particular app (e.g. booking) and should go into the appropriate
directory (e.g., libs/booking). We call them "app-specific". Even though such libraries can be used in
more than one place, the goal of creating them is not code reuse, but factoring the application into
well-defined modules to simplify the application’s maintenance.
A typical Nx workspace contains only four (4) types of libs: feature, data-access, ui, and util. You can
read about these types of libraries in detail in Part 2 of the book.
Interacting with Nx
Nx is configured for a workspace with these configuration files:
Nx also provides commands to work with your workspace, apps and libs. These can be entered into a
terminal or called from within the Angular Console, which is a graphical tool to interact with the
Angular CLI and Nx. The Angular Console is discussed in Appendix A.
In this book we provide the terminal commands using npm in each section as they are covered. We
provide instructions for the Angular Console where it is appropriate. All the commands are also listed
in Appendix B of the book.
5
yarn vs. npm
In this book we provide example code using npm. All of these commands also work with yarn. There
are some differences between the two:
1. npm commands can accept parameters for the command itself and for the underlying
command. For example, npm run task1 --watch — --param1=val1 passes the parameter watch to
npm itself and param1 to the underlying implementation of task1. The -- by itself indicates the
break after which all parameters are forwarded to the underlying task. yarn on the other hand
does not do this and passes all parameters to the underlying task; hence it doesn’t need the --
separator.
2. npm commands are run with npm run <command> and yarn commands are run with yarn <command>.
3. npm install installs the packages and you need to specify --save or --save-dev to save the
dependency; whereas yarn installs packages with yarn add and adds them to package.json by
default.
In the book, you can convert an npm script to work with yarn by using the following method:
1. If the command starts with npm run you can replace npm run with yarn. Otherwise, if the
command is npm install you can use yarn add instead (if it is npm install -g or npm install
--global, use yarn add global). For most of the other occurrences you should be able to swap npm
with yarn directly.
Creating a workspace
You can install Nx in the following way:
create-nx-workspace myworkspacename
This creates a new Nx Workspace using a sandboxed environment and running the Angular CLI’s ng
new command under the hood with the Nx schematics collection.
ng add @nrwl/schematics
6
Figure 1. Creating a new workspace in Angular Console
Regardless of how you create a workspace, what you end up with is a Nx Workspace with the
following files/folders created.
apps/
libs/
tools/
angular.json
nx.json
tslint.json
tsconfig.json
It’s similar to the standard Angular CLI projects with a few changes:
A short description of each of them is below; we expand on each of them as we progress through
7
the book.
A new Nx Workspace does not set up an initial application, but adding one is simple.
Creating an app
Adding new apps to an Nx Workspace is done by using the Angular CLI generate command. Nx has
a schematic named app that can be used to add a new app to our workspace:
3. It also provides an e2e sibling folder for this app that contains the e2e testing code.
Most of the options are identical to the ones supported by the default CLI application, but the
following are new or different: directory, routing, and tags.
• ng generate app myapp --tags=scope:shared,type:app annotates the created app with the two
tags, which can be used for advanced code analysis. Read more about this in the section
"Constraints on libraries" in Part 3.
8
Once we’ve created an application, we can start creating libs that contain all of the components and
logic that make up the application.
Creating a lib
Adding new libs to an Nx Workspace is done by using the Angular CLI generate command, just like
adding a new app.
This creates a new lib, places it in the libs directory, and configures the angular.json and
nx.json files to support the new lib.
Refer to the section "Notes on using libraries" for further description of the options.
Getting help
When using the terminal, all Nx commands offer the --help flag to display the available options and
descriptions for each. The Angular Console displays help text visually and also provides the list of
options grouped by whether they are required or not.
9
usage: ng generate lib <name> [options]
options:
--directory
A directory where the app is placed
--dryRun (-d)
Run through without making any changes.
--force (-f)
Forces overwriting of files.
--lazy
Add RouterModule.forChild when set to true, and a simple array of routes when
set to false.
--parent-module
Update the router configuration of the parent module using loadChildren or
children, depending on what `lazy` is set to.
--prefix (-p)
The prefix to apply to generated selectors.
--publishable
Generate a simple TS library when set to true.
--routing
Add router configuration. See lazy for more information.
--skip-format
Skip formatting files
--skip-package-json
Do not add dependencies to package.json.
--skip-ts-config
Do not update tsconfig.json for development experience.
--tags
Add tags to the library (used for linting)
--unit-test-runner
Test runner to use for unit tests
Getting help is also possible using the Angular Console, which provides all the options for each
command along with descriptions. It also separates the required options from the optional ones and
auto-fills the values based on your workspace (allowing you to choose from a list instead of manually
typing).
10
Figure 2. Getting help with Angular Console. Note that the options are separated into "important"
and
"optional", and that the values are able to be selected in a drop down.
Summary
This section sets the stage for the rest of the book. It covered key concepts to lay the groundwork for
the following:
• Some reasons for adopting a monorepo mindset were covered: increase visibility for all teams
into the codebase, reuse code directly instead of publishing and consuming via package
11
management, and ensure a consistent version of dependencies across all projects.
• The basic terminology and foundational concepts of Nx were presented (a workspace,
application and library)
12
Part 2: Organizing code with libraries
In a nutshell
Applications grow in size and complexity and in a large organization there are many ways to reuse
code to maintain consistency and to lower the development effort. Developing in a modular way
can be done using libraries, which are simply a collection of related files that perform a certain
task. These are composed together to make up an application.
Libraries require classifiers to describe their contents and intended purpose. These classifiers help to
organize the libraries and to provide a way to locate them.
Scope
Scope relates to a logical grouping, business use-case, or domain. Examples of scope from our
sample application are seatmap, booking, shared, and check-in. They contain libraries that manage a
sub-domain of application logic.
The following folder structure is an example scope hierarchy used to describe the seatmap
feature:
shared/
seatmap/
feature/
Here, "shared" and "seatmap" are grouping folders, and feature is a library that is nested
two levels deep. This offers a clear indication that this feature belongs to a domain of
seatmap which is a sub-domain of shared items.
The tag used in this library would be scope:shared, as this is the top-level scope.
Type
Type relates to the contents of the library and indicates its purpose and usage. Examples of types
are ui, data-access, and feature. See the longer discussion below.
We recommend using prefixes and tags to denote type. We recommend limiting the
number of types to only the four described in the sections to follow.
The folder name for this feature would be feature-shell so that it uses the prefix for its
library type.
The tag for the seatmap feature library as in the previous example would now be
scope:shared,type:feature.
13
Platform
There can be other classifiers used to differentiate between similar libraries (e.g. between
server, mobile, and desktop). Platform is one such classifier.
The final tag for the seatmap feature would be scope:shared,type:feature,platform:desktop.
Every library should be located in the folder tree by scope, have tags that are in the format
scope:SCOPE,type:TYPE,platform:PLATFORM, and have a prefix by its type.
traverse the folder tree multiple times to locate the needed files.
Rather we suggest that the code base be organized by domain and include all the
related files together e.g. airline which includes state, ui components, etc. inside a
single grouping folder.
Types of libraries
There are many different types of libraries in a workspace. In order to maintain a certain sense of
order, we recommend having only the below four (4) types of libraries:
• Utility libraries: A utility library contains common utilities and services used by many
libraries and applications. It also includes all the code related to State management.
Feature libraries
Most of the UI components in such a library are smart components that interact with the NgRx
Store. This type of library also contains most of the application logic, validation etc. Feature
libraries are almost always app-specific and are often lazy-loaded.
14
Naming Convention: feature or feature-* (e.g., feature-shell).
libs/
booking/
feature-shell/
src/
index.ts
lib/
booking-feature-shell.module.ts
@NgModule({
imports: [
CommonModule,
RouterModule.forChild([
{
path: '',
component: DashboardComponent,
pathMatch: 'full'
}
])
],
declarations: [DashboardComponent]
})
export class BookingFeatureShellModule {}
See the Notes on using Libraries for discussion over command line options available for lazy-
loading the feature libraries.
UI libraries
• A shared component library contains components used in multiple applications or across many
business functions.
15
There are two types of components when we build applications:
• Smart Components
• Presentational Components (aka dumb)
◦ no or very little business logic
order-create.component.ts
@Component({
selector: 'order-creator',
styleUrls: ['./order-create.component.css'],
template: `
<div fxLayout fxLayoutAlign="center center">
<order-card [order]="order"
[users]="users$ | async" >
</order-card>
</div>
`
})
export class OrderCreatorComponent {
users$: Observable<User[]> = this.service.users$;
order: Order = makeNewOrder();
order-card.component.ts
16
@Component({
selector: 'order-card',
styleUrls: ['./order-card.component.css'],
changeDetection: ChangeDetectionStrategy.OnPush,
template: `
<mat-card class="order-card" *ngIf="order">
...
</mat-card>
`
})
export class OrderCardComponent {
@Input() users: User[];
@Input() order: Order;
title(order): string {
const status = order.completed ? 'Finished' : 'Pending';
return !order.id ? 'Create New Order' : `${status} Order `;
}
}
@NgModule({
imports: [CommonModule],
declarations: [LoadingComponent, ConfirmButtonComponent],
exports: [LoadingComponent, ConfirmButtonComponent]
})
export class ComponentButtonsModule {}
Data-access libraries
Data-access libraries contain REST or webSocket services that function as client-side delegate layers
to server tier APIs. All files related to State management also reside in a data-access folder.
17
Example Data-Access Library Module:
@NgModule({
imports: [
CommonModule,
StoreModule.forFeature('customer', customersReducer, {
initialState: customersInitialState
}),
EffectsModule.forFeature([CustomersEffects])
],
providers: [CustomersEffects]
})
export class CustomersDataAccessModule {}
It’s easy to reuse data-access libraries, so focus on creating more shared data-access libraries.
Often building a shared data-access library requires you to get data from multiple endpoints. In this
case build an api where you specify exactly what fields you depend on, to indicate which endpoints
to hit (see the customers data access library).
When using NgRx facades, developers should consider three (3) levels of APIs:
• API for your HTTP REST services: these are simple, public methods invoked by views or facades.
These methods hide the complexity of using an internal HTTPClient service.
• API for your NgRx facades: api that is used only at the view component levels to — publish
query observables as properties: observables to NgRx store selectors — public methods that
internally dispatch actions to the NgRx store.
• API for your NgRx Effects: define which effects 'run' for which NgRx action. These effects
usually run asynchronous processes.
Utility libraries
A utility contains common utilities/services used by many libraries.
18
export { navigateToPage } from './src/navigate-to-page';
export { HttpMockBackend } from './src/http-mock-backend';
Grouping folders
In our reference structure, the folders libs/booking, libs/check-in, libs/shared, and
libs/shared/seatmap are grouping folders. They do not contain anything except other library or
grouping folders.
apps/
booking/
check-in/
libs/
booking/ <---- grouping folder
feature-shell/ <---- library
check-in/
feature-shell/
Shared libraries
In our reference example there are some shared libraries
libs/
shared/
data-access/ <---- shared library
seatmap/
data-access/ <---- shared library
feature-seatmap/ <---- shared library
Domain agnostic: Libraries that are used across most applications. These go into grouping folders
that describe the logical shared domain or into shared. (e.g., shared/data-access)
Domain-specific: Libraries that are only used in a few applications. These go into grouping folders.
(e.g., booking)
Sometimes, it is not easy to see if a library should be shared or not. See the decision trees in the
Reference section for some guidance.
19
Notes on using libraries
Flags Result
When creating lazy-loaded libraries, you need to add an entry to the tsconfig.app.json file of the
parent module app, so TypeScript knows to build it as well:
20
{
"include": [
"**/*.ts"
/* add all lazy-loaded libraries here: "../../../libs/my-lib/index.ts" */
, "../../../libs/mymodule/src/index.ts"
]
}
In most cases, Nx does it by default. Sometimes, you need to add this entry manually.
Firstly the folder is created (in a nested structure if --directory was specified). Next, the basic files
are generated for the library in this structure:
jest.config.js
src/
index.ts
lib/
shared-data-access.module.spec.ts
shared-data-access.module.ts
shared-data-access.service.spec.ts
shared-data-access.service.ts
test-setup.ts
tsconfig.lib.json
tsconfig.spec.json
tslint.json
NB: jest was selected as the testing framework for this lib.
Note the index.ts file is in the src/ folder. This is the barrel file for the lib. It contains the public
API to interact with the library and we should ensure that any constants, enums, classes, functions,
etc. that we want to expose are exported in this barrel file.
If it is nested inside src/ folder though, how does the barrel get exposed when we call the
library as follows: import { SharedDataAccessModule } from '@nrwl-airlines/shared/data-
access';? The import would expect the index.ts file to be in the root folder of the lib and not inside
src/.
Nx configures this in the root tsconfig.json, which would contain the following entry:
21
"paths": {
...
"@nrwl-airlines/shared/data-access": [
"libs/shared/data-access/src/index.ts"
]
...
}
Each app and lib that we create gets an entry in this file to help with the mapping using the @
workspace-relative path syntax. You can create your own aliases in this way if it helps with
development.
Import Paths
Components and classes contained within a library should be imported with relative paths only.
Referring to them with the workspace-relative path leads to linting errors.
TsLint rules have been configured to display errors for such violations.
Module names
The main module for a library must contain the full path (relative to libs) within the filename. e.g.
the main module for the library libs/booking/feature-destination would have the module filename
as booking-feature-destination.module.ts.
This is the way that the CLI creates the modules and we are recommending that we keep this
pattern.
Documenting libraries
The README should identify the purpose of the library and outline the public API for the library.
The document may also include other details such as:
• code owner
22
Summary
In this section we looked at libraries in depth. We discussed the following:
23
Part 3: Enforcing quality and consistency
Google enforces a constraint that there is a single version of Angular across all projects. This
ensures that there are no legacy projects that make it difficult to refactor, and enables developers to
move between projects as well.
Nx contains a few tools to help with maintaining consistency across the code-base and to ensure the
code quality:
• A single package.json ensures that all apps and libs use the same dependency versions.
• B uild tools like prettier assist with ensuring consistency in code formatting (one immediate
benefit is the removal of whitespace and formatting diffs in PRs).
• It allows for configurable enforcement of boundaries between libraries e.g. a util library not
being able to depend on a feature library (or even an app).
Code Formatting
At Google the general tenet is that anything that can be automated should be automated. One
of those things is code formatting. That’s why Nx comes with built-in support for Prettier.
There is absolutely nothing you need to configure. Just call format:write to format the affected files,
and format:check in the CI to guarantee that everything is formatted consistently.
The format:check command accepts the same options, but instead of formatting files, it throws if
any of the files aren’t formatted properly. This is useful for CI/CD.
24
You can also visualize what is affected by your change, by using the affected:dep-graph command.
We look at how to inform the affected commands which file changes to use to determine all
the apps and libraries that are affected by those code changes in Part 4 of the book. The dep-
graph responds by highlighting the affected apps an libs and to visually depict the critical path.
25
2. an app-specific library cannot depend on a lib from another app (e.g., "safe/" can only depend
on libs from "safe/" or shared libs)
• The IDEs and editors display an error if you are trying to violate these rules
• CI fails
To make constraints are defined and satisfied you need to provision tags when creating new libs:
Note, you can also modify the tags in nx.json after the fact, like this:
"booking-feature-destination": {
"tags": ["scope:booking", "type:feature"]
},
"booking-shell": {
"tags": []
},
Once tags have associated with each library, tsLint rules can be defined to configure constraints:
26
"nx-enforce-module-boundaries": [
true,
{
"allow": [],
"depConstraints": [
{
"sourceTag": "scope:shared", ①
"onlyDependOnLibsWithTags": ["scope:shared"]
},
{
"sourceTag": "scope:booking", ②
"onlyDependOnLibsWithTags": ["scope:booking", "scope:shared"]
},
{
"sourceTag": "type:util", ③
"onlyDependOnLibsWithTags": ["type:util"]
}
]
}
]
· A lib tagged scope:shared can only import from other libs with tag scope:shared.
· A lib tagged scope:booking can only import from libs tagged with either scope:booking or
scope:shared.
· A lib tagged type:util can only import from another lib that is tagged type:util.
With the example configuration above, we should see an error when we try to import private client
code from the admin part of our repo.
27
With dependency constraints, another team won’t create a dependency on your internal library.
You can define which projects contain components, NgRx code, and features, so you, for instance,
can disallow projects containing presentational UI components from depending on NgRx. You can
define which projects are experimental and which are stable, so stable applications cannot depend
on experimental projects etc.
Implicit Dependencies
Nx uses its built-in intelligence to create the dependency graph of the apps and libs, and that gets
used to figure out what needs to be rebuilt and retested. There are certain files, however, that Nx
cannot analyze. That’s why Nx has support for implicit dependencies. They are defined in nx.json.
{
"npmScope": "mycompany",
"implicitDependencies": {
"package.json": "*",
"angular.json": "*",
"tsconfig.json": "*",
"tslint.json": "*",
"nx.json": "*"
},
"projects": {}
}
The ”package.json”: “*” line tells Nx that a change to package.json affects every single project. Since
the root-level README file isn’t listed, changing it won’t affect anything.
28
We can be more specific and list the projects that are affected by a particular file.
{
"npmScope": "mycompany",
"implicitDependencies": {
"package.json": "*",
"angular.json": "*",
"tsconfig.json": "*",
"tslint.json": "*",
"nx.json": "*",
"tools/scripts/app1-rename-bundles.js": ["app1"]
},
"projects": {}
}
In addition to being able to list implicit dependencies between files and projects, we can also add
implicit dependencies between projects. For instance, an Nx workspace can contain the backend
code for our Angular app. The back-end code implicitly depends on the Angular app, but it is not
expressed in the code and so cannot be deduced by Nx. We can explicitly define it as follows:
{
"npmScope": "mycompany",
"implicitDependencies": {
"package.json": "*",
"angular.json": "*",
"tsconfig.json": "*",
"tslint.json": "*",
"nx.json": "*",
"tools/scripts/app1-rename-bundles.js": ["app1"]
},
"projects": {
"app": {},
"backend": {
"implicitDependencies": ["app"]
}
}
}
Exceptions
As with everything there are exceptions, which can also add to your tslint.json.
29
"nx-enforce-module-boundaries": [
true,
{
"allow": ["@myworkspace/mylib"],
"depConstraints": [
]
}
]
Workspace Schematics
Some best practices are well-established in the Angular community. For instance, most non-trivial
Angular projects use NgRx, so Nx comes with a set of built-in code generators and runtime libraries
to make sure your applications' state management is consistent. Just run ng g ngrx accounts
--module=one/two/app.module.ts to set everything up (read more [here](https://nrwl.io/nx/guide-
setting-up-ngrx)).
Some, however, are specific to your organization. Nx can help with those as well. Say, for example,
we want to promote a pattern of encapsulating NgRx-related code into data-access libraries. This is
how easy it is to do it with Nx:
30
Finally, invoke it to generate a new data-access lib.
31
Handling back-end calls with DataPersistence
Managing state is a hard problem. We need to coordinate multiple backends, web workers, and UI
components, all of which update the state concurrently.
What should we store in memory vs. the URL? What about the local UI state? How do we
synchronize the persistent state, the URL, and the state on the server? All these questions have to be
answered when designing the state management of our applications. Nx Data Persistence is a set of
helper functions that enables the developer to manage state with an intentional
synchronization strategy and handle error state. Refer to Using NgRx 4 to Manage State in Angular
Applications for more detailed example of the state problem Data Persistence is solving. [1:
https://blog.nrwl.io/using-ngrx-4-to-manage-state-in-angular-applications-64e7a1f84b7b]
1. Fetching the state from the Store to retrieve one or more values during the asynchronous
operation
Optimistic Updates
For a better user experience, optimisticUpdate method updates the state on the client
application immediately before updating the data on the server-side. While it addresses fetching
data in order, removing the race conditions and handling error, it is optimistic about not failing
to update the server. In case of a failure, when using optimisticUpdate, the local state on the client
is already updated. The developer must provide an undo action to restore the previous state to keep
it consistent with the server state. The error handling must be done in the callback, or by means of
the undo action.
32
import { DataPersistence } from '@nrwl/nx';
...
class TodoEffects {
@Effect() updateTodo = this.dataPersistence.optimisticUpdate('UPDATE_TODO', {
// provides an action and the current state of the store
run: (a: UpdateTodo, state: TodosState) => {
return this.backend(state.user, a.payload);
},
constructor(
private dataPersistence: DataPersistence<TodosState>,
private backend: Backend
) {}
}
Pessimistic Updates
To achieve a more reliable data synchronization, pessimisticUpdate method updates the server data
first before updating the UI. When the change is reflected in the server state, a change is made
in the client state by dispatching an action. The pessimisticUpdate method enforces the order of
the fetches and error handling.
33
import { DataPersistence } from '@nrwl/nx';
...
@Injectable()
class TodoEffects {
@Effect() updateTodo = this.dataPersistence.pessimisticUpdate('UPDATE_TODO', {
// provides an action and the current state of the store
run: (a: UpdateTodo, state: TodosState) => {
// update the backend first, and then dispatch an action that
// updates the client side
return this.backend(state.user, a.payload).map(updated => ({
type: 'TODO_UPDATED',
payload: updated
}));
},
constructor(
private dataPersistence: DataPersistence<TodosState>,
private backend: Backend
) {}
}
Data Fetching
DataPersistence’s fetch method provides consistency when fetching data. If there are multiple
requests scheduled for the same action it only runs the last one.
34
import { DataPersistence } from '@nrwl/nx';
...
@Injectable()
class TodoEffects {
@Effect() loadTodos = this.dataPersistence.fetch('GET_TODOS', {
// provides an action and the current state of the store
run: (a: GetTodos, state: TodosState) => {
return this.backend(state.user, a.payload).map(r => ({
type: 'TODOS',
payload: r
}));
},
constructor(
private dataPersistence: DataPersistence<TodosState>,
private backend: Backend
) {}
This is correct, but we can improve the performance by supplying and id of the data by using an
accessor function and adding concurrency to the fetch action for different ToDo’s.
35
@Injectable()
class TodoEffects {
@Effect() loadTodo = this.dataPersistence.fetch('GET_TODO', {
id: (a: GetTodo, state: TodosState) => {
return a.payload.id;
},
constructor(
private dataPersistence: DataPersistence<TodosState>,
private backend: Backend
) {}
With this setup, the requests for Todo run concurrently with the requests for Todo 2. Consecutive
calls for Todo are queued behind the first.
Since the user can always interact with the URL directly, we should treat the router as the source of
truth and the initiator of actions. In other words, the router should invoke the reducer, not the
other way around.
When our state depends on navigation, we can not assume the route change happened when a
new url is triggered but when we actually know the user was able to navigate to the url.
DataPersistence navigation method checks if an activated router state contains the passed in
component type, and, if it does, runs the run callback. It provides the activated snapshot associated
with the component and the current state. And it only runs the last request.
36
import { DataPersistence } from '@nrwl/nx';
...
@Injectable()
class TodoEffects {
@Effect() loadTodo = this.dataPersistence.navigation(TodoComponent, {
run: (a: ActivatedRouteSnapshot, state: TodosState) => {
return this.backend.fetchTodo(a.params['id']).map(todo => ({
type: 'TODO_LOADED',
payload: todo
}));
},
constructor(
private dataPersistence: DataPersistence<TodosState>,
private backend: Backend
) {}
37
Part 4: Helping with builds and CI
Rebuilding and retesting only affected apps and libs
When we consider large repositories that contain many dozens and hundreds of libs and many
apps, we realize that it would be very difficult to have to test and build all of them whenever code
new is merged. It doesn’t scale. Neither does a manual trace fo the dependencies: it would be
unreliable. It would be great if there was a reliable way for us to only test and build the affected
libraries and apps.
Nx uses code analysis to determine what needs to be rebuild and retested. It provides this via the
affected commands: affected:build, affected:test, and affected:e2e. These commands can be run
with the following options to determine only those libraries and apps (aka. "projects") that are
affected by your code changes.
• Compare changes between 2 git commits: You can run --base=SHA1 --head=SHA2, where SHA1
is the one you want to compare with and SHA2 contains the changes. This generates a list of
files that we can use to determine which projects are affected by those changes and only
process those.
• Uncommited changes: Use the flag --uncommited to use all the uncommitted changes on
the current branch to obtain the list of affected projects to process (files that were changed since
the last commit: you can use git status to view them)
• Untracked: Use the flag --untracked to use the list of only untracked files (files that were created
since the last commit) to determine the projects to process (useful during development)
• Explicit files: Use --files to provide an explicit comma-delimited list of files (useful during
development)
• All projects: Use --all to force the command for all projects instead of only those affected by
code changes
• Only last failed: Use --only-failed to isolate only those projects which previously failed
(default: false)
Additional options
A walkthrough
Let’s take as an example our sample repo (nrwl-airlines). Its dependency graph is as below:
38
39
Scenario 1: Change in a app-specific library
If we change a file in the check-in-data-access library, we can now see that it only affects check-in-
feature-shell, check-in-desktop, and check-in-mobile.
40
Scenario 2: Change in a shared library
If we now change a file in the shared-data-access library, we see that it affects all the projects in the
workspace! The impact is large and so we know that we have to take extra care and to reach out to
the other projects' owners to make sure that everyone is aware of the changes.
The difference in Scenario 1 and Scenario 2 is pretty clear: rebuilding or retesting only the affected
projects can have a huge impact on the amount of time that the builds and tests take. We also don’t
have to deploy changes to projects that haven’t changed.
41
Part 5: Development Challenges in
a Monorepo
Here are some of the common challenges with moving into a monorepo:
1. How do we manage releases to various environments when there are many teams and
applications in the same repository?
3. How do we organize the shared code so that it is reusable without incurring too much technical
debt and without needing constant maintenance?
We looked at Issue #3 in Part 2 of the book (Organizing code with Libraries). The rest of this section
focuses on how to allow teams to work together in a single repository and still have the flexibility to
deploy different applications.
Our repo has 4 applications that need to be deployed: desktop and mobile versions of booking and
check-in. All of the apps have an implicit dependency on a shared-data-access library.
Listed below are some of the common challenges that can present themselves:
1. The booking team makes changes to shared-data-access: how can they communicate
the changes to the check-in team to ensure that nothing is broken?
2. The check-in team failed a QA check and need to make a code fix; however there is new code in
the repo from the booking team.
3. The seatmap team discovers a bug in prod: how can we hotfix this and ensure that the fix is also
42
in our codebase?
4. The builds and tests take a long time in CI: how can we reduce the amount of time?
5. How can we ensure that our trunk branch is deployable? Should we even adopt trunk-based
development?
1. Ensure that the repository settings disallow plain merges and instead only allow rebased (or
fast-forward) merges. This ensures that the developer has all of the latest changes (and has
tested the code) from the shared branch before merging the PR.
2. Ensure that PRs are very small in scope. This makes the risk a lot lower and also allows for
better review (through code review as well as manual testing).
3. Use feature toggles so that features that are still in development are not visible to the end user
(See below section).
4. Be aware (and make others aware) of changes to shared code and minimize the risk in the
following way:
3. Issue a deprecation warning for the old method/class/library with a set expiry time
4. Work with the other teams to help them migrate to the new version
Code owners
One of the other mitigation strategies is to assign code owners for the libs in the Nx workspace. A
code owner (is usually a group rather than a single individual) is responsible for all changes to a lib,
and would orchestrate the process outlined above to deprecate and migrate to newer versions of
the code.
Working with a code owner for shared code removes some of the guesswork and helps to formulate
a plan when it comes to modifying shared code. There are fewer surprises when it comes time for
integration.
Feature toggles
There are two types of feature toggles: build-time and run-time. B uild-time toggles are
recommended for initialization settings (connection/URL settings, layout settings, etc.) which would
be needed when the application loads, so that we are not waiting for a network request to complete
before rendering the app.
Run-time feature-toggles are usually implemented via network calls to a settings file hosted on the
server. These are meant for scenarios where we want to control the settings without rebuilding the
43
application. It needs careful management for handling the caching behavior.
Trunk-based development
We generally encourage trunk-based development because we believe in the basics
tenets:
• B ranches should be short-lived and should be very specific to accomplishing a single piece of
work
• We believe that all code should be as up-to-date as possible with other teams so that there is no
lengthy integration process
• All code that is being merged into the trunk branch has already been tested at a basic level
There are some organizational challenges to implementing this: release deadlines, teams that need
to work in isolation as part of a research or secret effort, picking up unexpected changes in shared
code when trying to implement a separate feature, etc.
B elow is our recommendation for working with a monorepo. It needs to be tailored to your
organization, but can offer some guidance on what works in general for most organizations.
• Always use Pull Requests when merging code. A PR has two purposes:
• To initiate a build and to run tests and lint checks to ensure that we don’t break the build
• Feature flags
• Feature branches
44
Figure 3. Trunk based Development
45
Summary
In this section we took a brief look at some of the common issues that teams run into when
adopting a monorepo workflow.
• Some strategies to deal with overlapping code between teams: smaller PRs, requiring that PRs
ar eup-to-date with the target branch, and a way to version the code so that there is a transition
plan.
• Setting up code owners for various libs so that there is a responsibility both for merging code
into that library and also to promote the changes responsibly to the affected teams.
• Feature flags allow teams to tests their code in environments without affecting or being affected
by other teams. Run-time flags avs. build-time strategies were discussed.
46
Appendix A: Other environments
Users can choose to use a graphical UI for Nx and the Angular CLI. The Angular Console allows
developers to interact with Nx in a visual way. All the CLI options are visible and interactive, and
commands can be previewed and executed directly within the interface (the commands are
executed using the Angular CLI under the hood).
Features
Quick actions
Apps listed in the workspace view have quick-access buttons to serve, build, test and create
components. Further actions are found under Run tasks in the left menu.
Libs have quick actions for test and create component. Further actions would also be found
47
under Run tasks in the left menu.
Generate schematics
We can generate any of the schematics detected in the workspace (including custom ones) by
picking from the grouped list.
Option completion
There is the ability to pick from a list of choices instead of manual typing. This comes in handy
when specifying a path to a module since you don’t need to type in the path manually. Also,
toggles are visual, allowing you to avoid having to decide between true, false, --no-option, etc.
48
Figure 8. All options and expected output
Third-party extensions
Third-party extensions can be added from a curated list.
49
Figure 10. Add third-party extensions
50
Appendix B: Commands
We have covered many commands in this book. We can group the commands into two larger
groups: those provided by the CLI and those provided by Nx; and we can divide the latter even
further by those that can target specific files using affected and those that can’t or don’t need to.
• test
• lint
• e2e
• workspace-schematic
All of these commands can be run using npm e.g. npm run lint.
Scripts provided by Nx
format
Nx adds support for prettier and to format your files according to the prettier settings. There are
two commands:
• format:check lists all the files that do not conform to the rules in prettier
update
Nx allows you to update it with this command. update:check does a check only and informs you if
an update is needed.
dep-graph
Nx displays a graphical view of the project dependencies between apps and libs. This is useful to
understand which projects may be affected by changes that you make to a lib or app.
Affected commands
Affected commands contain a prefix of affected: and target specific files for a given action.
Supported actions are below:
• affected:apps
• affected:libs
• affected:build
51
• affected:e2e
• affected:test
• affected:lint
• affected:dep-graph
• --uncommited: all the files that have been modified since checking out this branch
• specific files: a comma-delimited list of files
• only-failed: only use the files from the last failed job
52
Appendix C: How-tos
Updating Nx
If you created an Nx Workspace using Nx 6.0.0 and then decided to upgrade the version of Nx, the Nx
update command can handle modifying the configuration file and the source files as needed to get
your workspace configuration to match the requirements of the new version of Nx.
53
Should I reuse or create a feature library?
To answer that question, ask yourself:
Create a new feature library or modify an existing feature library. All (most) feature libraries are
app-specific, so you need to select a application section directory to put it.
• Copy the content of the app (everything except index.html, main.ts, polyfills.ts, app.module.ts,
app.component.ts and other "global" files) into the lib.
• Update app.module.ts to remove all the imports and declarations that are no longer needed.
Leave forRoot calls there, but we may have to update them (e.g., StoreModule.forRoot({})).
54
imported once, and it should be done at the app level.
• Create an integration test that exercises the new module without requiring the containing app.
@NgModule({
exports: [
MatButtonModule,
MatCardModule,
MatIconModule,
MatProgressBarModule,
BidiModule
]
})
export class CustomMaterialModule {}
Any library or app using these components [in their templates] simply imports
the CustomMaterialModule. When additional components are needed, only
this CustomMaterialModule needs to be modified to add the required imports and exports.
55
Get in touch to scale up your
development capacity
Reimagine how you solve your toughest challenges and build an amazing application with the
greatest development partner, best tools and latest practices. Build software for the future with
Nrwl. Our modern open-source development tools and practices enable software teams to
achieve better collaboration and build long-term team capabilities and success. Contact us, at
hello@nrwl.io about your project or fill out the form at https://nrwl.io/services. We look forward to
hearing from you!