Building Angular Apps in an Nx Monorepo
In this tutorial you'll learn how to use Angular with Nx in a monorepo (integrated) setup.
What will you learn?
- how to create a new Angular application
- how to run a single task (i.e. serve your app) or run multiple tasks in parallel
- how to leverage code generators to scaffold components
- how to modularize your codebase and impose architectural constraints for better maintainability
- how to speed up CI with Nx Cloud ⚡
Note, this tutorial sets up a repo with applications and libraries in their own subfolders. If you are looking for an Angular standalone app setup then check out our Angular standalone app tutorial.
Nx CLI vs. Angular CLI
Nx evolved from being an extension of the Angular CLI to a fully standalone CLI working with multiple frameworks. As a result, adopting Nx as an Angular user is relatively straightforward. Your existing code, including builders and schematics, will still work as before, but you'll also have access to all the benefits Nx offers.
Advantages of Nx over the Angular CLI:
- Cache any target
- Run only tasks affected by a code change
- Split a large angular.json into multiple project.json files
- Integrate with modern tools
- Controllable update process
Visit our "Nx and the Angular CLI" page for more details.
Final Code
Here's the source code of the final result for this tutorial.
Example repository/nrwl/nx-recipes/tree/main/angular-monorepo
Creating a new Angular Monorepo
Create a new Angular monorepo with the following command:
~❯
npx create-nx-workspace@latest angular-monorepo --preset=angular-monorepo
1
2NX Let's create a new workspace [https://nx.dev/getting-started/intro]
3
4✔ Application name · angular-store
5✔ Which bundler would you like to use? · esbuild
6✔ Default stylesheet format · css
7✔ Do you want to enable Server-Side Rendering (SSR) and Static Site Generation (SSG/Prerendering)? · No
8✔ Test runner to use for end to end (E2E) tests · cypress
9✔ Which CI provider would you like to use? · github
10
Let's name the initial application angular-store
. In this tutorial we're going to use cypress
for e2e tests and css
for styling. We'll talk more about how Nx integrates with GitHub Actions later in the tutorial. The above command generates the following structure:
1└─ angular-monorepo
2 ├─ ...
3 ├─ apps
4 │ ├─ angular-store
5 │ │ ├─ src
6 │ │ │ ├─ app
7 │ │ │ │ ├─ app.component.css
8 │ │ │ │ ├─ app.component.html
9 │ │ │ │ ├─ app.component.spec.ts
10 │ │ │ │ ├─ app.component.ts
11 │ │ │ │ ├─ app.config.ts
12 │ │ │ │ ├─ app.routes.ts
13 │ │ │ │ └─ nx-welcome.component.ts
14 │ │ │ ├─ assets
15 │ │ │ ├─ index.html
16 │ │ │ ├─ main.ts
17 │ │ │ ├─ styles.css
18 │ │ │ └─ test-setup.ts
19 │ │ ├─ eslintrc.json
20 │ │ ├─ jest.config.ts
21 │ │ ├─ project.json
22 │ │ ├─ tsconfig.app.json
23 │ │ ├─ tsconfig.editor.json
24 │ │ ├─ tsconfig.json
25 │ │ └─ tsconfig.spec.json
26 │ └─ angular-store-e2e
27 │ └─ ...
28 ├─ nx.json
29 ├─ tsconfig.base.json
30 └─ package.json
31
The setup includes:
- a new Angular application (
apps/angular-store/
) - a Cypress based set of e2e tests (
apps/angular-store-e2e/
) - Prettier preconfigured
- ESLint preconfigured
- Jest preconfigured
Typically, an integrated Nx workspace places application projects in the apps
folder and library projects in the libs
folder. Applications are encouraged to be as light-weight as possible so that more code is pushed into libraries and can be reused in other projects. This folder structure is just a suggestion and can be modified to suit your organization's needs.
The nx.json
file contains configuration settings for Nx itself and global default settings that individual projects inherit. The apps/angular-store/project.json
file contains settings that are specific to the angular-store
project. We'll examine that file more in the next section.
Serving the App
To serve your new Angular application, just run:
❯
npx nx serve angular-store
Your application should be served at http://localhost:4200.
Nx uses the following syntax to run tasks:
Manually Defined Tasks
The project tasks are defined in the project.json
file.
1{
2 "name": "angular-store",
3 ...
4 "targets": {
5 "build": { ... },
6 "serve": { ... },
7 "extract-i18n": { ... },
8 "lint": { ... },
9 "test": { ... },
10 "serve-static": { ... },
11 },
12}
13
Each target contains a configuration object that tells Nx how to run that target.
1{
2 "name": "angular-store",
3 ...
4 "targets": {
5 "serve": {
6 "executor": "@angular-devkit/build-angular:dev-server",
7 "defaultConfiguration": "development",
8 "options": {
9 "buildTarget": "angular-store:build"
10 },
11 "configurations": {
12 "development": {
13 "buildTarget": "angular-store:build:development",
14 "hmr": true
15 },
16 "production": {
17 "buildTarget": "angular-store:build:production",
18 "hmr": false
19 }
20 }
21 },
22 ...
23 },
24}
25
The most critical parts are:
executor
- this is of the syntax<plugin>:<executor-name>
, where theplugin
is an NPM package containing an Nx Plugin and<executor-name>
points to a function that runs the task.options
- these are additional properties and flags passed to the executor function to customize it
Learn more about how to run tasks with Nx. We'll revisit running tasks later in this tutorial.
Adding Another Application
Nx plugins usually provide generators that allow you to easily scaffold code, configuration or entire projects. To see what capabilities the @nx/angular
plugin provides, run the following command and inspect the output:
angular-monorepo❯
npx nx list @nx/angular
1
2NX Capabilities in @nx/angular:
3
4 GENERATORS
5
6 add-linting : Adds linting configuration to an Angular project.
7 application : Creates an Angular application.
8 component : Generate an Angular Component.
9 component-cypress-spec : Creates a Cypress spec for a UI component that has a story.
10 component-story : Creates a stories.ts file for a component.
11 component-test : Creates a cypress component test file for a component.
12 convert-tslint-to-eslint : Converts a project from TSLint to ESLint.
13 init : Initializes the `@nx/angular` plugin.
14 library : Creates an Angular library.
15 library-secondary-entry-point : Creates a secondary entry point for an Angular publishable library.
16 remote : Generate a Remote Angular Module Federation Application.
17 move : Moves an Angular application or library to another folder within the workspace and updates the project configuration.
18 // etc...
19
20 EXECUTORS/BUILDERS
21
22 delegate-build : Delegates the build to a different target while supporting incremental builds.
23 ng-packagr-lite : Builds a library with support for incremental builds.
24This executor is meant to be used with buildable libraries in an incremental build scenario. It is similar to the `@nx/angular:package` executor but with some key differences:
25- It doesn't run `ngcc` automatically (`ngcc` needs to be run separately beforehand if needed, this can be done in a `postinstall` hook on `package.json`).
26- It only produces ESM2020 bundles.
27- It doesn't generate package exports in the `package.json`.
28 package : Builds and packages an Angular library producing an output following the Angular Package Format (APF) to be distributed as an NPM package.
29This executor is similar to the `@angular-devkit/build-angular:ng-packagr` with additional support for incremental builds.
30 // etc...
31
If you prefer a more integrated experience, you can install the "Nx Console" extension for your code editor. It has support for VSCode, IntelliJ and ships a LSP for Vim. Nx Console provides autocompletion support in Nx configuration files and has UIs for browsing and running generators.
More info can be found in the integrate with editors article.
Run the following command to generate a new inventory
application. Note how we append --dry-run
to first check the output.
angular-monorepo❯
npx nx g @nx/angular:app inventory --directory=apps/inventory --dry-run
1NX Generating @nx/angular:application
2
3✔ Would you like to configure routing for this application? (y/N) · false
4✔ Would you like to use Standalone Components? (y/N) · true
5CREATE apps/inventory/project.json
6CREATE apps/inventory/src/assets/.gitkeep
7CREATE apps/inventory/src/favicon.ico
8CREATE apps/inventory/src/index.html
9CREATE apps/inventory/src/styles.css
10CREATE apps/inventory/tsconfig.app.json
11CREATE apps/inventory/tsconfig.editor.json
12CREATE apps/inventory/tsconfig.json
13CREATE apps/inventory/src/app/app.component.css
14CREATE apps/inventory/src/app/app.component.html
15CREATE apps/inventory/src/app/app.component.spec.ts
16CREATE apps/inventory/src/app/app.component.ts
17CREATE apps/inventory/src/app/app.config.ts
18CREATE apps/inventory/src/app/nx-welcome.component.ts
19CREATE apps/inventory/src/main.ts
20CREATE apps/inventory/.eslintrc.json
21CREATE apps/inventory/jest.config.ts
22CREATE apps/inventory/src/test-setup.ts
23CREATE apps/inventory/tsconfig.spec.json
24CREATE apps/inventory-e2e/cypress.config.ts
25CREATE apps/inventory-e2e/src/e2e/app.cy.ts
26CREATE apps/inventory-e2e/src/fixtures/example.json
27CREATE apps/inventory-e2e/src/support/app.po.ts
28CREATE apps/inventory-e2e/src/support/commands.ts
29CREATE apps/inventory-e2e/src/support/e2e.ts
30CREATE apps/inventory-e2e/tsconfig.json
31CREATE apps/inventory-e2e/project.json
32CREATE apps/inventory-e2e/.eslintrc.json
33
34NOTE: The "dryRun" flag means no changes were made.
35
As you can see, it generates a new application in the apps/inventory/
folder. Let's actually run the generator by removing the --dry-run
flag.
❯
npx nx g @nx/angular:app inventory --directory=apps/inventory
Sharing Code with Local Libraries
When you develop your Angular application, usually all your logic sits in the app
folder. Ideally separated by various folder names which represent your "domains". As your app grows, however, the app becomes more and more monolithic and the code is unable to be shared with other applications.
1└─ angular-monorepo
2 ├─ ...
3 ├─ apps
4 │ └─ angular-store
5 │ ├─ ...
6 │ ├─ src
7 │ │ ├─ app
8 │ │ │ ├─ products
9 │ │ │ ├─ cart
10 │ │ │ ├─ ui
11 │ │ │ ├─ ...
12 │ │ │ └─ app.tsx
13 │ │ ├─ ...
14 │ │ └─ main.tsx
15 │ ├─ ...
16 │ └─ project.json
17 ├─ nx.json
18 ├─ ...
19
Nx allows you to separate this logic into "local libraries". The main benefits include
- better separation of concerns
- better reusability
- more explicit "APIs" between your "domain areas"
- better scalability in CI by enabling independent test/lint/build commands for each library
- better scalability in your teams by allowing different teams to work on separate libraries
Creating Local Libraries
Let's assume our domain areas include products
, orders
and some more generic design system components, called ui
. We can generate a new library for each of these areas using the Angular library generator:
1npx nx g @nx/angular:library products --directory=libs/products --standalone
2npx nx g @nx/angular:library orders --directory=libs/orders --standalone
3npx nx g @nx/angular:library shared-ui --directory=libs/shared/ui --standalone
4
Note how we type out the full path in the directory
flag to place the libraries into a subfolder. You can choose whatever folder structure you like to organize your projects. If you change your mind later, you can run the move generator to move a project to a different folder.
Running the above commands should lead to the following directory structure:
1└─ angular-monorepo
2 ├─ ...
3 ├─ apps
4 ├─ libs
5 │ ├─ products
6 │ │ ├─ ...
7 │ │ ├─ project.json
8 │ │ ├─ src
9 │ │ │ ├─ index.ts
10 │ │ │ ├─ test-setup.ts
11 │ │ │ └─ lib
12 │ │ │ └─ products
13 │ │ ├─ tsconfig.json
14 │ │ ├─ tsconfig.lib.json
15 │ │ └─ tsconfig.spec.json
16 │ ├─ orders
17 │ │ ├─ ...
18 │ │ ├─ project.json
19 │ │ ├─ src
20 │ │ │ ├─ index.ts
21 │ │ │ └─ ...
22 │ │ └─ ...
23 │ └─ shared
24 │ └─ ui
25 │ ├─ ...
26 │ ├─ project.json
27 │ ├─ src
28 │ │ ├─ index.ts
29 │ │ └─ ...
30 │ └─ ...
31 ├─ ...
32
Each of these libraries
- has its own
project.json
file with corresponding targets you can run (e.g. running tests for just orders:npx nx test orders
) - has the name you specified in the generate command; you can find the name in the corresponding
project.json
file - has a dedicated
index.ts
file which is the "public API" of the library - is mapped in the
tsconfig.base.json
at the root of the workspace
Importing Libraries into the Angular Applications
All libraries that we generate automatically have aliases created in the root-level tsconfig.base.json
.
1{
2 "compilerOptions": {
3 ...
4 "paths": {
5 "@angular-monorepo/orders": ["libs/orders/src/index.ts"],
6 "@angular-monorepo/products": ["libs/products/src/index.ts"],
7 "@angular-monorepo/shared-ui": ["libs/shared/ui/src/index.ts"]
8 },
9 ...
10 },
11}
12
Hence we can easily import them into other libraries and our Angular application. As an example, let's use the pre-generated ProductsComponent
component from our libs/products
library.
You can see that the ProductsComponent
is exported via the index.ts
file of our products
library so that other projects in the repository can use it. This is our public API with the rest of the workspace. Only export what's really necessary to be usable outside the library itself.
1export * from './lib/products/products.component';
2
We're ready to import it into our main application now. First (if you haven't already), let's set up the Angular router. Configure it in the app.config.ts
.
1import { ApplicationConfig } from '@angular/core';
2import {
3 provideRouter,
4 withEnabledBlockingInitialNavigation,
5} from '@angular/router';
6import { appRoutes } from './app.routes';
7
8export const appConfig: ApplicationConfig = {
9 providers: [provideRouter(appRoutes, withEnabledBlockingInitialNavigation())],
10};
11
And in app.component.html
:
1<router-outlet></router-outlet>
2
Then we can add the ProductsComponent
component to our app.routes.ts
and render it via the routing mechanism whenever a user hits the /products
route.
1import { Route } from '@angular/router';
2import { NxWelcomeComponent } from './nx-welcome.component';
3
4export const appRoutes: Route[] = [
5 {
6 path: '',
7 component: NxWelcomeComponent,
8 pathMatch: 'full',
9 },
10 {
11 path: 'products',
12 loadComponent: () =>
13 import('@angular-monorepo/products').then((m) => m.ProductsComponent),
14 },
15];
16
Serving your app (npx nx serve angular-store
) and then navigating to /products
should give you the following result:
Let's apply the same for our orders
library.
- import the
OrdersComponent
fromlibs/orders
into theapp.routes.ts
and render it via the routing mechanism whenever a user hits the/orders
route
In the end, your app.routes.ts
should look similar to this:
1import { Route } from '@angular/router';
2import { NxWelcomeComponent } from './nx-welcome.component';
3
4export const appRoutes: Route[] = [
5 {
6 path: '',
7 component: NxWelcomeComponent,
8 pathMatch: 'full',
9 },
10 {
11 path: 'products',
12 loadComponent: () =>
13 import('@angular-monorepo/products').then((m) => m.ProductsComponent),
14 },
15 {
16 path: 'orders',
17 loadComponent: () =>
18 import('@angular-monorepo/orders').then((m) => m.OrdersComponent),
19 },
20];
21
Let's also show products in the inventory
app.
1import { Component } from '@angular/core';
2import { ProductsComponent } from '@angular-monorepo/products';
3
4({
5 standalone: true,
6 imports: [ProductsComponent],
7 selector: 'app-root',
8 templateUrl: './app.component.html',
9 styleUrls: ['./app.component.css'],
10})
11export class AppComponent {
12 title = 'inventory';
13}
14
1<lib-products></lib-products>
2
Visualizing your Project Structure
Nx automatically detects the dependencies between the various parts of your workspace and builds a project graph. This graph is used by Nx to perform various optimizations such as determining the correct order of execution when running tasks like npx nx build
, identifying affected projects and more. Interestingly you can also visualize it.
Just run:
❯
npx nx graph
You should be able to see something similar to the following in your browser.
Notice how shared-ui
is not yet connected to anything because we didn't import it in any of our projects.
Exercise for you: change the codebase such that shared-ui
is used by orders
and products
. Note: you need to restart the npx nx graph
command to update the graph visualization or run the CLI command with the --watch
flag.
Testing and Linting
Our current setup not only has targets for serving and building the Angular application, but also has targets for unit testing, e2e testing and linting. The test
and lint
targets are defined in the application project.json
file, while the e2e
target is inferred from the apps/angular-store-e2e/cypress.config.ts
file. We can use the same syntax as before to run these tasks:
1npx nx test angular-store # runs the tests for angular-store
2npx nx lint inventory # runs the linter on inventory
3npx nx e2e angular-store-e2e # runs e2e tests for the angular-store
4
Inferred Tasks
Nx identifies available tasks for your project from tooling configuration files, package.json
scripts and the targets defined in project.json
. All tasks from the angular-store
project are defined in its project.json
file, but the companion angular-store-e2e
project has its tasks inferred from configuration files. To view the tasks that Nx has detected, look in the Nx Console, Project Details View or run:
❯
npx nx show project angular-store-e2e --web
angular-store-e2e
Root: apps/angular-store-e2e
Type:application
Targets
e2e-ci
nx:noop
Cacheablee2e-ci--src/e2e/app.cy.ts
cypress run --env webServerCommand="nx run angular-store:serve-static" --spec src/e2e/app.cy.ts
Cacheable
e2e
cypress run
Cacheablelint
eslint .
Cacheable
If you expand the e2e
task, you can see that it was created by the @nx/cypress
plugin by analyzing the apps/angular-store-e2e/cypress.config.ts
file. Notice the outputs are defined as:
1[
2 [
3 "{workspaceRoot}/dist/cypress/apps/angular-store-e2e/videos",
4 "{workspaceRoot}/dist/cypress/apps/angular-store-e2e/screenshots"
5 ]
6]
7
This value is being read from the videosFolder
and screenshotsFolder
defined by the nxE2EPreset
in your apps/angular-store-e2e/cypress.config.ts
file. Let's change their value in your apps/angular-store-e2e/cypress.config.ts
file:
1// ...
2export default defineConfig({
3 e2e: {
4 ...nxE2EPreset(__filename, {
5 // ...
6 }),
7 baseUrl: 'http://localhost:4200',
8 videosFolder: '../dist/cypress/apps/angular-store-e2e/videos-changed',
9 screenshotsFolder:
10 '../dist/cypress/apps/angular-store-e2e/screenshots-changed',
11 },
12});
13
Now if you look at the project details view again, the outputs for the e2e
target will be:
1[
2 "{workspaceRoot}/apps/dist/cypress/apps/angular-store-e2e/videos-changed",
3 "{workspaceRoot}/apps/dist/cypress/apps/angular-store-e2e/screenshots-changed"
4]
5
This feature ensures that Nx will always cache the correct files.
You can also override the settings for inferred tasks by modifying the targetDefaults
in nx.json
or setting a value in your project.json
file. Nx will merge the values from the inferred tasks with the values you define in targetDefaults
and in your specific project's configuration.
Running Multiple Tasks
In addition to running individual tasks, you can also run multiple tasks in parallel using the following syntax:
❯
npx nx run-many -t test lint e2e
Caching
One thing to highlight is that Nx is able to cache the tasks you run.
Note that all of these targets are automatically cached by Nx. If you re-run a single one or all of them again, you'll see that the task completes immediately. In addition, (as can be seen in the output example below) there will be a note that a matching cache result was found and therefore the task was not run again.
angular-monorepo❯
npx nx run-many -t test lint e2e
1✔ nx run e2e:lint [existing outputs match the cache, left as is]
2✔ nx run angular-store:lint [existing outputs match the cache, left as is]
3✔ nx run angular-store:test [existing outputs match the cache, left as is]
4✔ nx run e2e:e2e [existing outputs match the cache, left as is]
5
6——————————————————————————————————————————————————————
7
8NX Successfully ran targets test, lint, e2e for 5 projects (54ms)
9
10Nx read the output from the cache instead of running the command for 10 out of 10 tasks.
11
Not all tasks might be cacheable though. You can configure which tasks are cacheable in the project configuration or in the global Nx configuration. You can also learn more about how caching works.
Testing Affected Projects
Commit your changes to git.
❯
git commit -a -m "some commit message"
And then make a small change to the products
library.
1<p>product-list works!</p>
2<p>This is a change. 👋</p>
3
One of the key features of Nx in a monorepo setting is that you're able to run tasks only for projects that are actually affected by the code changes that you've made. To run the tests for only the projects affected by this change, run:
❯
npx nx affected -t test
Note that the unit tests were run for products
, angular-store
and inventory
, but not for orders
because a change to products
can not possibly break the tests for orders
. In a small repo like this, there isn't a lot of time saved, but as there are more tests and more projects, this quickly becomes an essential command.
You can also see what projects are affected in the graph visualizer with;
❯
npx nx graph --affected
Building the Apps for Deployment
If you're ready and want to ship your applications, you can build them using
angular-monorepo❯
npx nx run-many -t build
1NX Generating @nx/angular:component
2
3CREATE libs/orders/src/lib/order-list/order-list.component.css
4CREATE libs/orders/src/lib/order-list/order-list.component.html
5CREATE libs/orders/src/lib/order-list/order-list.component.spec.ts
6CREATE libs/orders/src/lib/order-list/order-list.component.ts
7UPDATE libs/orders/src/index.ts
8❯ nx run-many -t build
9
10✔ nx run inventory:build:production (7s)
11✔ nx run angular-store:build:production (7s)
12
13———————————————————————————————————————————————————————————————————————
14
15NX Successfully ran target build for 2 projects (7s)
16
All the required files will be placed in dist/apps/angular-store
and dist/apps/inventory
and can be deployed to your favorite hosting provider.
You can even create your own deploy
task that sends the build output to your hosting provider.
1{
2 "targets": {
3 "deploy": {
4 "dependsOn": "build",
5 "command": "netlify deploy --dir=dist/angular-store"
6 }
7 }
8}
9
Replace the command
with whatever terminal command you use to deploy your site.
The "dependsOn": "build"
setting tells Nx to make sure that the project's build
task has been run successfully before the deploy
task.
With the deploy
tasks defined, you can deploy a single application with npx nx deploy angular-store
or deploy any applications affected by the current changes with:
❯
npx nx affected -t deploy
Imposing Constraints with Module Boundary Rules
Once you modularize your codebase you want to make sure that the libs are not coupled to each other in an uncontrolled way. Here are some examples of how we might want to guard our small demo workspace:
- we might want to allow
orders
to import fromshared-ui
but not the other way around - we might want to allow
orders
to import fromproducts
but not the other way around - we might want to allow all libraries to import the
shared-ui
components, but not the other way around
When building these kinds of constraints you usually have two dimensions:
- type of project: what is the type of your library. Example: "feature" library, "utility" library, "data-access" library, "ui" library
- scope (domain) of the project: what domain area is covered by the project. Example: "orders", "products", "shared" ... this really depends on the type of product you're developing
Nx comes with a generic mechanism that allows you to assign "tags" to projects. "tags" are arbitrary strings you can assign to a project that can be used later when defining boundaries between projects. For example, go to the project.json
of your orders
library and assign the tags type:feature
and scope:orders
to it.
1{
2 ...
3 "tags": ["type:feature", "scope:orders"],
4}
5
Then go to the project.json
of your products
library and assign the tags type:feature
and scope:products
to it.
1{
2 ...
3 "tags": ["type:feature", "scope:products"],
4}
5
Finally, go to the project.json
of the shared-ui
library and assign the tags type:ui
and scope:shared
to it.
1{
2 ...
3 "tags": ["type:ui", "scope:shared"],
4}
5
Notice how we assign scope:shared
to our UI library because it is intended to be used throughout the workspace.
Next, let's come up with a set of rules based on these tags:
type:feature
should be able to import fromtype:feature
andtype:ui
type:ui
should only be able to import fromtype:ui
scope:orders
should be able to import fromscope:orders
,scope:shared
andscope:products
scope:products
should be able to import fromscope:products
andscope:shared
To enforce the rules, Nx ships with a custom ESLint rule. Open the .eslintrc.base.json
at the root of the workspace and add the following depConstraints
in the @nx/enforce-module-boundaries
rule configuration:
1{
2 ...
3 "overrides": [
4 {
5 ...
6 "rules": {
7 "@nx/enforce-module-boundaries": [
8 "error",
9 {
10 "enforceBuildableLibDependency": true,
11 "allow": [],
12 "depConstraints": [
13 {
14 "sourceTag": "*",
15 "onlyDependOnLibsWithTags": ["*"]
16 },
17 {
18 "sourceTag": "type:feature",
19 "onlyDependOnLibsWithTags": ["type:feature", "type:ui"]
20 },
21 {
22 "sourceTag": "type:ui",
23 "onlyDependOnLibsWithTags": ["type:ui"]
24 },
25 {
26 "sourceTag": "scope:orders",
27 "onlyDependOnLibsWithTags": [
28 "scope:orders",
29 "scope:products",
30 "scope:shared"
31 ]
32 },
33 {
34 "sourceTag": "scope:products",
35 "onlyDependOnLibsWithTags": ["scope:products", "scope:shared"]
36 },
37 {
38 "sourceTag": "scope:shared",
39 "onlyDependOnLibsWithTags": ["scope:shared"]
40 }
41 ]
42 }
43 ]
44 }
45 },
46 ...
47 ]
48}
49
To test it, go to your libs/products/src/lib/product-list/product-list.component.ts
file and import the OrdersComponent
from the orders
project:
1import { Component } from '@angular/core';
2import { CommonModule } from '@angular/common';
3
4// This import is not allowed 👇
5import { OrdersComponent } from '@angular-monorepo/orders';
6
7({
8 selector: 'angular-monorepo-product-list',
9 standalone: true,
10 imports: [CommonModule],
11 templateUrl: './product-list.component.html',
12 styleUrls: ['./product-list.component.css'],
13})
14export class ProductsComponent {}
15
If you lint your workspace you'll get an error now:
❯
npx nx run-many -t lint
1NX Running target lint for 7 projects
2✖ nx run products:lint
3 Linting "products"...
4
5 /Users/isaac/Documents/code/nx-recipes/angular-monorepo/libs/products/src/lib/product-list/product-list.component.ts
6 5:1 error A project tagged with "scope:products" can only depend on libs tagged with "scope:products", "scope:shared" @nx/enforce-module-boundaries
7 5:10 warning 'OrdersComponent' is defined but never used @typescript-eslint/no-unused-vars
8
9 ✖ 2 problems (1 error, 1 warning)
10
11 Lint warnings found in the listed files.
12
13 Lint errors found in the listed files.
14
15
16✔ nx run orders:lint (1s)
17✔ nx run angular-store:lint (1s)
18✔ nx run angular-store-e2e:lint (689ms)
19✔ nx run inventory-e2e:lint (690ms)
20✔ nx run inventory:lint (858ms)
21✔ nx run shared-ui:lint (769ms)
22
23———————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————
24
25NX Ran target lint for 7 projects (3s)
26
27✔ 6/7 succeeded [0 read from cache]
28
29✖ 1/7 targets failed, including the following:
30 - nx run products:lint
31
If you have the ESLint plugin installed in your IDE you should immediately see an error:
Learn more about how to enforce module boundaries.
Fast CI ⚡
Make sure you have completed the previous sections of this tutorial before starting this one. If you want a clean starting point, you can check out the reference code as a starting point.
This tutorial walked you through how Nx can improve the local development experience, but the biggest difference Nx makes is in CI. As repositories get bigger, making sure that the CI is fast, reliable and maintainable can get very challenging. Nx provides a solution.
- Nx reduces wasted time in CI with the
affected
command. - Nx Replay's remote caching will reuse task artifacts from different CI executions making sure you will never run the same computation twice.
- Nx Agents efficiently distribute tasks across machines ensuring constant CI time regardless of the repository size. The right number of machines is allocated for each PR to ensure good performance without wasting compute.
- Nx Atomizer automatically splits large e2e tests to distribute them across machines. Nx can also automatically identify and rerun flaky e2e tests.
Connect to Nx Cloud
Nx Cloud is a companion app for your CI system that provides remote caching, task distribution, e2e tests deflaking, better DX and more.
Now that we're working on the CI pipeline, it is important for your changes to be pushed to a GitHub repository.
- Commit your existing changes with
git add . && git commit -am "updates"
- Create a new GitHub repository
- Follow GitHub's instructions to push your existing code to the repository
When we set up the repository at the beginning of this tutorial, we chose to use GitHub Actions as a CI provider. This created a basic CI pipeline and configured Nx Cloud in the repository. It also printed a URL in the terminal to register your repository in your Nx Cloud account. If you didn't click on the link when first creating your repository, you can show it again by running:
❯
npx nx connect
Once you click the link, follow the steps provided and make sure Nx Cloud is enabled on the main branch of your repository.
Configure Your CI Workflow
When you chose GitHub Actions as your CI provider at the beginning of the tutorial, create-nx-workspace
created a .github/workflows/ci.yml
file that contains a CI pipeline that will run the lint
, test
, build
and e2e
tasks for projects that are affected by any given PR. Since we are using Nx Cloud, the pipeline will also distribute tasks across multiple machines to ensure fast and reliable CI runs.
If you need to generate a new workflow file for GitHub Actions or other providers, you can do so with this command:
❯
npx nx generate ci-workflow
The key lines in the CI pipeline are:
1name: CI
2# ...
3jobs:
4 main:
5 runs-on: ubuntu-latest
6 steps:
7 - uses: actions/checkout@v4
8 with:
9 fetch-depth: 0
10 # This enables task distribution via Nx Cloud
11 # Run this command as early as possible, before dependencies are installed
12 # Learn more at https://nx.dev/ci/reference/nx-cloud-cli#npx-nxcloud-startcirun
13 # Connect your workspace by running "nx connect" and uncomment this
14 - run: npx nx-cloud start-ci-run --distribute-on="3 linux-medium-js" --stop-agents-after="build"
15 - uses: actions/setup-node@v3
16 with:
17 node-version: 20
18 cache: 'npm'
19 - run: npm ci --legacy-peer-deps
20 - uses: nrwl/nx-set-shas@v4
21 # Nx Affected runs only tasks affected by the changes in this PR/commit. Learn more: https://nx.dev/ci/features/affected
22 - run: npx nx affected -t lint test build
23
Open a Pull Request
Commit the changes and open a new PR on GitHub.
❯
git add .
❯
git commit -m 'add CI workflow file'
❯
git push origin add-workflow
When you view the PR on GitHub, you will see a comment from Nx Cloud that reports on the status of the CI run.
The See all runs
link goes to a page with the progress and results of tasks that were run in the CI pipeline.
For more information about how Nx can improve your CI pipeline, check out one of these detailed tutorials:
Next Steps
Here's some things you can dive into next:
- Read more about how Nx compares to the Angular CLI
- Learn more about the underlying mental model of Nx
- Learn about popular generators such as how to setup Tailwind
- Learn how to migrate your existing Angular CLI repo to Nx
- Setup Storybook for our shared UI library
Also, make sure you
- Join the Official Nx Discord Server to ask questions and find out the latest news about Nx.
- Follow Nx on Twitter to stay up to date with Nx news
- Read our Nx blog
- Subscribe to our Youtube channel for demos and Nx insights