Building and Testing Angular Apps in Nx

In this tutorial, you'll learn how to create a new Angular monorepo using the Nx platform.

Prerequisites

This tutorial requires a GitHub account to demonstrate the full value of Nx - including task running, caching, and CI integration.

What will you learn?

  • how to create a new Angular workspace with GitHub Actions preconfigured
  • how to run a single task (i.e. serve your app) or run multiple tasks in parallel
  • how to modularize your codebase with local libraries for better code organization
  • how to benefit from caching that works both locally and in CI
  • how to set up self-healing CI to apply fixes directly from your local editor

Creating a new Angular Monorepo

To get started, let's create a new Angular monorepo with Nx Cloud and GitHub Actions preconfigured.

Create a new Angular monorepoWith Nx and GitHub Actions fully set up

You should now have a new workspace called acme with Angular CLI, Jest, ESLint, and Prettier preconfigured.

1acme 2├── .github 3│ └── workflows 4│ └── ci.yml 5├── apps 6│ └── demo 7├── README.md 8├── eslint.config.mjs 9├── nx.json 10├── package-lock.json 11├── package.json 12├── tsconfig.base.json 13└── vitest.workspace.ts 14

The .github/workflows/ci.yml file preconfigures your CI in GitHub Actions to run build and test through Nx.

The demo app is created under the apps directory as a convention. Later in this tutorial we'll create libraries under the packages folder. In practice, you can choose any folder structure you like.

The nx.json file contains configuration settings for Nx itself and global default settings that individual projects inherit.

Before continuing, it is important to make sure that your GitHub repository is connected to your Nx Cloud organization to enable remote caching and self-healing in CI.

Checkpoint: Workspace Created and Connected

At this point you should have:

  1. A new Nx workspace on your local machine with an Angular app in apps/demo
  2. A new GitHub repository for the workspace with .github/workflows/ci.yml pipeline preconfigured
  3. A workspace in Nx Cloud that is connected to the GitHub repository

You should see your workspace in your Nx Cloud organization.

If you do not see your workspace in Nx Cloud then please follow the steps outlined in the Nx Cloud setup.

Now, let's build some features and see how Nx helps get us to production faster.

Serving the App

To serve your new Angular app, run:

npx nx serve demo

The app is served at http://localhost:4200.

Nx uses the following syntax to run tasks:

Syntax for Running Tasks in Nx

Project Configuration

The project tasks are defined in the project.json file.

apps/demo/project.json
1{ 2 "name": "demo", 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.

project.json
1{ 2 "name": "angular-store", 3 ... 4 "targets": { 5 "serve": { 6 "executor": "@angular/build: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 the plugin 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

To view all tasks for a project, look in the Nx Console project detail view or run:

npx nx show project demo

Project Details View (Simplified)

demo

Root: apps/demo

Type:Application

Targets

  • build

    @angular/build:application

    Cacheable

Modularization with Local Libraries

When you develop your Angular application, usually all your logic sits in the app's src folder. Ideally separated by various folder names which represent your domains or features. As your app grows, however, the app becomes more and more monolithic, which makes building and testing it harder and slower.

1acme 2├── apps 3│ └── demo 4│ └── src 5│ ├── app 6│ ├── cart 7│ ├── products 8│ ├── orders 9│ └── ui 10└── ... 11

Nx allows you to separate this logic into "local libraries." The main benefits include

  • better separation of concerns
  • better reusability
  • more explicit private and public boundaries (APIs) between domains and features
  • 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

Create Local Libraries

Let's create a reusable design system library called ui that we can use across our workspace. This library will contain reusable components such as buttons, inputs, and other UI elements.

1npx nx g @nx/angular:library packages/ui --unitTestRunner=vitest 2

Note how we type out the full path in the command to place the library into a subfolder. You can choose whatever folder structure you like to organize your projects.

Running the above command should lead to the following directory structure:

1acme 2├── apps 3│ └── demo 4├── packages 5│ └── ui 6├── eslint.config.mjs 7├── nx.json 8├── package-lock.json 9├── package.json 10├── tsconfig.base.json 11└── vitest.workspace.ts 12

Just as with the demo app, Nx automatically infers the tasks for the ui library from its configuration files. You can view them by running:

npx nx show project ui

In this case, we have the lint and test tasks available, among other inferred tasks.

npx nx lint ui

npx nx test ui

Import Libraries into the Demo App

All libraries that we generate are automatically included in the TypeScript path mappings configured in the root-level tsconfig.base.json.

tsconfig.base.json
1{ 2 "compilerOptions": { 3 ... 4 "paths": { 5 "@acme/ui": ["packages/ui/src/index.ts"] 6 }, 7 ... 8 }, 9} 10

Hence, we can easily import them into other libraries and our Angular application.

You can see that the Ui component is exported via the index.ts file of our ui library so that other projects in the repository can use it. This is our public API with the rest of the workspace and is enforced by the library's build configuration. Only export what's necessary to be usable outside the library itself.

packages/ui/src/index.ts
1export * from './lib/ui/ui'; 2

Let's add a simple Hero component that we can use in our demo app.

packages/ui/src/lib/hero/hero.ts
1import { Component, Input, Output, EventEmitter } from '@angular/core'; 2import { CommonModule } from '@angular/common'; 3 4@Component({ 5 selector: 'lib-hero', 6 standalone: true, 7 imports: [CommonModule], 8 template: ` 9 <div [ngStyle]="containerStyle"> 10 <h1 [ngStyle]="titleStyle">{{ title }}</h1> 11 <p [ngStyle]="subtitleStyle">{{ subtitle }}</p> 12 <button (click)="handleCtaClick()" [ngStyle]="buttonStyle"> 13 {{ cta }} 14 </button> 15 </div> 16 `, 17}) 18export class Hero { 19 @Input() title!: string; 20 @Input() subtitle!: string; 21 @Input() cta!: string; 22 @Output() ctaClick = new EventEmitter<void>(); 23 24 containerStyle = { 25 backgroundColor: '#1a1a2e', 26 color: 'white', 27 padding: '100px 20px', 28 textAlign: 'center', 29 }; 30 31 titleStyle = { 32 fontSize: '48px', 33 marginBottom: '16px', 34 }; 35 36 subtitleStyle = { 37 fontSize: '20px', 38 marginBottom: '32px', 39 }; 40 41 buttonStyle = { 42 backgroundColor: '#0066ff', 43 color: 'white', 44 border: 'none', 45 padding: '12px 24px', 46 fontSize: '18px', 47 borderRadius: '4px', 48 cursor: 'pointer', 49 }; 50 51 handleCtaClick() { 52 this.ctaClick.emit(); 53 } 54} 55

Then, export it from index.ts.

packages/ui/src/index.ts
1export * from './lib/hero/hero'; 2export * from './lib/ui/ui'; 3

We're ready to import it into our main application now.

apps/demo/src/app/app.ts
1import { Component } from '@angular/core'; 2import { RouterOutlet } from '@angular/router'; 3// importing the component from the library 4import { Hero } from '@acme/ui'; 5 6@Component({ 7 selector: 'app-root', 8 standalone: true, 9 imports: [RouterOutlet, Hero], 10 template: ` 11 <lib-hero 12 title="Welcmoe demo" 13 subtitle="Build something amazing today" 14 cta="Get Started" 15 ></lib-hero> 16 `, 17}) 18export class App {} 19

Serve your app again (npx nx serve demo) and you should see the new Hero component from the ui library rendered on the home page.

If you have keen eyes, you may have noticed that there is a typo in the App component. This mistake is intentional, and we'll see later how Nx can fix this issue automatically in CI.

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.

Loading...

Let's create a git branch with the new hero component so we can open a pull request later:

git checkout -b add-hero-component

git add .

git commit -m 'add hero component'

Testing and Linting - Running Multiple Tasks

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. We can use the same syntax as before to run these tasks:

1npx nx test demo # runs the tests for demo 2npx nx lint ui # runs the linter on ui 3

More conveniently, we can also run tasks in parallel using the following syntax:

npx nx run-many -t test lint

This is exactly what is configured in .github/workflows/ci.yml for the CI pipeline. The run-many command allows you to run multiple tasks across multiple projects in parallel, which is particularly useful in a monorepo setup.

There is a test failure for the demo app due to the updated content. Don't worry about it for now, we'll fix it in a moment with the help of Nx Cloud's self-healing feature.

Local Task Cache

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.

~/acme

npx nx run-many -t test lint

1 ✔ nx run ui:lint 2 ✔ nx run ui:test 3 ✔ nx run demo:lint 4 ✖ nx run demo:test 5 6————————————————————————————————————————————————————————————————————————————————————————————————————————— 7 8 NX Ran targets test, lint for 2 projects (1s) 9 10 ✔ 3/4 succeeded [3 read from cache] 11 12 ✖ 1/4 targets failed, including the following: 13 14 - nx run demo:test 15

Again, the demo:test task failed, but notice that the remaining three tasks were read from cache.

Not all tasks might be cacheable though. You can configure the cache settings in the targetDefaults property of the nx.json file. You can also learn more about how caching works.

Self-Healing CI with Nx Cloud

In this section, we'll explore how Nx Cloud can help your pull request get to green faster with self-healing CI. Recall that our demo app has a test failure, so let's see how this can be automatically resolved.

The npx nx-cloud fix-ci command that is already included in your GitHub Actions workflow (github/workflows/ci.yml)is responsible for enabling self-healing CI and will automatically suggest fixes to your failing tasks.

.github/workflows/ci.yml
1name: CI 2 3on: 4 push: 5 branches: 6 - main 7 pull_request: 8 9permissions: 10 actions: read 11 contents: read 12 13jobs: 14 main: 15 runs-on: ubuntu-latest 16 steps: 17 - uses: actions/checkout@v4 18 with: 19 filter: tree:0 20 fetch-depth: 0 21 22 - uses: actions/setup-node@v4 23 with: 24 node-version: 20 25 cache: 'npm' 26 27 - run: npm ci --legacy-peer-deps 28 29 - run: npx nx run-many -t lint test build 30 31 - run: npx nx-cloud fix-ci 32 if: always() 33

You will also need to install the Nx Console editor extension for VS Code, Cursor, or IntelliJ. For the complete AI setup guide, see our AI integration documentation.

Visual Studio Code

Install Nx Console for VSCodeThe official VSCode extension for Nx.

IntelliJ IDEA

Now, let's push the add-hero-component branch to GitHub and open a new pull request.

git push origin add-hero-component

# Don't forget to open a pull request on GitHub

As expected, the CI check fails because of the test failure in the demo app. But rather than looking at the pull request, Nx Console notifies you that the run has completed, and that it has a suggested fix for the failing test. This means that you don't have to waste time babysitting your PRs, and the fix can be applied directly from your editor.

Nx Console with failure notification

Fix CI from Your Editor

From the Nx Console notification, you can click Show Suggested Fix button. Review the suggested fix, which in this case is to change the typo Welcmoe to the correct Welcome spelling. Approve this fix by clicking ApplyFix and that's it!

Suggestion to fix the typo in the editor

You didn't have to leave your editor or do any manual work to fix it. This is the power of self-healing CI with Nx Cloud.

Remote Cache for Faster Time To Green

After the fix has been applied and committed, CI will re-run automatically, and you will be notified of the results in your editor.

Tasks with remote cache hit

When you click View Results to show the run in Nx Cloud, you'll notice something interesting. The lint and test tasks for the ui library were read from remote cache and did not have to run again, thus each taking less than a second to complete.

Nx Cloud run showing remote cache hits

This happens because Nx Cloud caches the results of tasks and reuses them across different CI runs. As long as the inputs for each task have not changed (e.g. source code), then their results can be replayed from Nx Cloud's Remote Cache. In this case, since the last fix was applied only to the demo app's source code, none of the tasks for ui library had to be run again.

This significantly speeds up the time to green for your pull requests, because subsequent changes to them have a good chance to replay tasks from cache.

Remote Cache Outputs

Outputs from cached tasks, such as the dist folder for builds or coverage folder for tests, are also read from cache. Even though a task was not run again, its outputs are available. The Cache Task Results page provides more details on caching.

This pull request is now ready to be merged with the help of Nx Cloud's self-healing CI and remote caching.

Pull request is green

Next Steps

Here are some things you can dive into next:

Also, make sure you