A Quick Start to the Modern Full-Stack Project Framework Nx

A Quick Start to the Modern Full-Stack Project Framework Nx
A Quick Start to the Modern Full-Stack Project Framework Nx
Nx is an integrated full-stack engineering framework that provides a complete set of solutions and tools for full-stack project development. Nx brings together modern tools such as Cypress, Jest, Prettier, and TypeScript to support integrated development across frameworks including Angular, React, Node, Nest, and Express. It also offers features such as dependency graphs and impact analysis, enabling even small teams to use development tools and workflows similar to those at Google, Facebook, and Microsoft. This article is based on the official Nx getting started example and is intended to help you become familiar with the Nx workflow and features.
1. Create a new Nx workspace
Use any of the following commands. Note that installing Cypress can be very slow in mainland China, so it is best to adjust the download path in the .npmrc file. Choose the empty preset template and select Angular CLI as the command-line tool. This will create an empty workspace.
npx create-nx-workspace@latest myworkspace
npm init nx-workspace myworkspace
yarn create nx-workspace myworkspace
cd myworkspace
2. Create an Angular application
If you already have Angular CLI installed (recommended), you can use the ng command to get started quickly. First add Angular with the default options, then create an application named todos.
ng add @nrwl/angular --defaults
ng g @nrwl/angular:application todos
This command generates an empty Angular application and its corresponding E2E test project. Run the app with:
ng serve todos
3. Add tests
Nx uses Cypress for E2E testing by default. Modify apps/todos-e2e/src/support/app.po.ts:
export const getTodos = () => cy.get('li.todo');
export const getAddTodoButton = () => cy.get('button#add-todo');
Then modify apps/todos-e2e/src/integration/app.spec.ts:
import { getAddTodoButton, getTodos } from '../support/app.po';
describe('TodoApps', () => {
beforeEach(() => cy.visit('/'));
it('should display todos', () => {
getTodos().should(t => expect(t.length).equal(2));
getAddTodoButton().click();
getTodos().should(t => expect(t.length).equal(3));
});
});
Stop ng serve, then run:
ng e2e todos-e2e
or
ng e2e todos-e2e --watch
This will launch the test runner UI. If you add the --headless flag, it will run in a headless browser without showing the frontend UI.
4. Modify the Angular app to pass the test
Open apps/todos/src/app/app.component.ts and change it to:
import { Component } from '@angular/core';
interface Todo {
title: string;
}
@Component({
selector: 'myorg-root',
templateUrl: './app.component.html',
styleUrls: ['./app.component.css']
})
export class AppComponent {
todos: Todo[] = [{ title: 'Todo 1' }, { title: 'Todo 2' }];
addTodo() {
this.todos.push({
title: `New todo ${Math.floor(Math.random() * 1000)}`
});
}
}
Open apps/todos/src/app/app.component.html and change it to:
<h1>Todos</h1>
<ul>
<li class="todo" *ngFor="let t of todos">{{ t.title }}</li>
</ul>
<button id="add-todo" (click)="addTodo()">Add Todo</button>
Run the test again from the previous step and you will see that it now passes. If you started Cypress with --watch, the test will rerun automatically.
5. Connect to an API service
Import HttpClientModule to use HTTP services. Add the import in apps/todos/src/app/app.module.ts, and declare it in the imports section of @NgModule.
import { BrowserModule } from '@angular/platform-browser';
import { NgModule } from '@angular/core';
import { AppComponent } from './app.component';
import { HttpClientModule } from '@angular/common/http';
@NgModule({
declarations: [AppComponent],
imports: [BrowserModule, HttpClientModule],
providers: [],
bootstrap: [AppComponent]
})
export class AppModule {}
Then modify apps/todos/src/app/app.component.ts to use HTTP:
import { Component } from '@angular/core';
import { HttpClient } from '@angular/common/http';
interface Todo {
title: string;
}
@Component({
selector: 'myorg-root',
templateUrl: './app.component.html',
styleUrls: ['./app.component.css']
})
export class AppComponent {
todos: Todo[] = [];
constructor(private http: HttpClient) {
this.fetch();
}
fetch() {
this.http.get('/api/todos').subscribe(t => (this.todos = t));
}
addTodo() {
this.http.post('/api/addTodo', {}).subscribe(() => {
this.fetch();
});
}
}
Run ng serve. Since the backend does not yet provide the API endpoints, no data will appear on the page.
6. Add backend API endpoints
This example uses Nest to provide backend API services. Nest is an excellent backend framework and is widely used in many backend projects. Run the following command to generate an API service. When prompted for the directory, just press Enter directly; there is no need to create an extra api directory level.
ng g @nrwl/nest:app api --frontendProject=todos
Under the apps directory in the workspace, you will now see the frontend project folder todos, the backend project folder api, and the frontend E2E project todos-e2e, among others. In an integrated Nx full-stack workspace, this structure allows Nx to manage and run the frontend, backend, and test projects together in one place.
You can use the following commands for the backend project:
ng serve api— run the backend appng build api— build the backend appng test api— run backend tests
Open apps/api/src/app/app.service.ts and add the service logic.
Open apps/api/src/app/app.module.ts and make sure it contains:
import { Module } from '@nestjs/common';
import { AppController } from './app.controller';
import { AppService } from './app.service';
@Module({
imports: [],
controllers: [AppController],
providers: [AppService]
})
export class AppModule {}
Then open apps/api/src/app/app.controller.ts and add the API endpoints:
import { Controller, Get, Post } from '@nestjs/common';
import { AppService } from './app.service';
@Controller()
export class AppController {
constructor(private readonly appService: AppService) {}
@Get('todos')
getData() {
return this.appService.getData();
}
@Post('addTodo')
addTodo() {
return this.appService.addTodo();
}
}
Now open http://localhost:3333/api/todos in the browser and you should see the default todos data. If you use VS Code, you can test the POST http://localhost:3333/api/addtodo endpoint with the Rest Client extension. You can also use curl, Postman, or other tools. After sending a POST request and then a GET request again, you will see that a new Todo item has been added.
7. Configure the proxy
In the previous step, when adding the API service, we used the --frontendProject=todos parameter. This actually enables Angular to access the backend during development. The key configuration is in the root angular.json file:
{
"serve": {
"builder": "@angular-devkit/build-angular:dev-server",
"options": {
"browserTarget": "todos:build",
"proxyConfig": "apps/todos/proxy.conf.json"
},
"configurations": {
"production": {
"browserTarget": "todos:build:production"
}
}
}
}
The proxyConfig entry specifies the proxy file. Open that file:
{
"/api": {
"target": "http://localhost:3333",
"secure": false
}
}
This forwards all /api requests to the backend API service automatically. Now if you run ng serve api in one terminal and ng serve todos in another, then open http://127.0.0.1:4200, you will see that the todos app is working correctly.
8. Share code
At this point the frontend and backend are working well, but Nx’s architectural strengths have not yet been fully demonstrated. The Todo interface is declared separately on both the frontend and backend. As the project grows, this duplication can gradually cause integration problems. So we use Nx to create a data library for shared types.
Run the following command from the workspace root:
ng g @nrwl/workspace:lib data
Open /libs/data/src/lib/data.ts and define the Todo interface:
export interface Todo {
title: string;
}
Then import the Todo interface from the shared library in both the backend file apps/api/src/app/app.service.ts and the frontend file apps/todos/src/app/app.component.ts, replacing the local declarations.
import { Todo } from '@myorg/data';
9. Create a shared library (lib)
Nx shared libraries are not only for sharing code; they can also be used to organize code into better modules through public APIs and factory-style patterns.
UI library
Here we will illustrate this by creating an Angular component. Run the following command to create an Angular UI shared library:
ng g @nrwl/angular:lib ui
This creates the libs/ui/ library. In libs/ui/src/lib/ui.module.ts, Nx initializes code like this:
import { NgModule } from '@angular/core';
import { CommonModule } from '@angular/common';
@NgModule({
imports: [CommonModule]
})
export class UiModule {}
Create a component
Next, create an Angular component named todos inside the ui library:
ng g component todos --project=ui --export
As in a normal Angular app, modify the generated libs/ui/src/lib/todos/todos.component.ts and todos.component.html files. Note that this also uses the Todo interface created in the previous section.
// todos.component.ts
import { Component, OnInit, Input } from '@angular/core';
import { Todo } from '@myorg/data';
@Component({
selector: 'myorg-todos',
templateUrl: './todos.component.html',
styleUrls: ['./todos.component.css']
})
export class TodosComponent implements OnInit {
@Input() todos: Todo[];
constructor() {}
ngOnInit() {}
}
<!-- todos.component.html -->
<ul>
<li class="todo" *ngFor="let t of todos">{{ t.title }}</li>
</ul>
Use the UI library
Replace the component created earlier with the one from the generated ui library. Update apps/todos/src/app/app.module.ts to import the module:
import { BrowserModule } from '@angular/platform-browser';
import { NgModule } from '@angular/core';
import { AppComponent } from './app.component';
import { HttpClientModule } from '@angular/common/http';
import { UiModule } from '@myorg/ui';
@NgModule({
declarations: [AppComponent],
imports: [BrowserModule, HttpClientModule, UiModule],
providers: [],
bootstrap: [AppComponent]
})
export class AppModule {}
Update app.component.html to use the shared component:
<h1>Todos</h1>
<myorg-todos [todos]="todos"></myorg-todos>
<button id="add-todo" (click)="addTodo()">Add Todo</button>
Generate a dependency graph with Dep Graph
A large project can contain hundreds or thousands of components and applications, and their dependencies can quickly become difficult to understand. Nx provides a dedicated command-line tool, Dep Graph, to visualize these relationships, making them much easier to inspect.
Run the following command. Nx will open http://127.0.0.1:4211 in the browser and generate the architecture graph.
npm run dep-graph
Check affected dependencies
In addition to generating dependency graphs, Nx can also check which projects are affected by changes. Before trying this step, you need Git installed. If Git is not installed on your system, install it first. Also, since this project was downloaded from an existing Git repository, if you want to push it to a remote repository of your own, you should reset the remote target first.
Commit the changes made so far:
git add .
git commit -am 'init'
Next, modify some source code—for example, add an erroneous ! before a symbol in libs/ui/src/lib/todos/todos.component.html. Then run:
npm run affected:apps
The system will list the affected apps, for example todos, indicating that this change impacts the dependent app.
If you run the following command, it will show the affected shared libraries instead (in this case, ui):
npm run affected:libs
Affected tests
Besides manually checking which applications and libraries are impacted, you can also run affected tests to see whether the changes break any tests. Since we changed the application code without updating the corresponding test files, the tests will fail in this case.
npm run affected:test
Running affected tests will start the test process again and list test issues. If you only want to see the failing tests, you can pass the --only-failed parameter.
Parallel testing
To speed up test execution, you can pass the --parallel parameter to run tests concurrently.
Affected builds
After code changes, you can also perform an affected build to quickly build only the impacted parts.
npm run affected:build # affected build
npm run affected -- -- target=build # equivalent to the command above
Summary
Through this project, we accomplished the following:
- Built a full-stack application using Angular for the frontend and Nest for the backend
- Shared code between the frontend and backend
- Created a UI library
- Used Nx’s dependency graph tool to generate a project dependency graph
- Used Nx’s affected analysis tools to detect dependency-related changes


