Photograph at the top by Edgar Chaparro, originally published on Unsplash.
This piece first appeared on 2020-05-22.
You're reading an entry from the Angular Architectural Patterns tutorial series.
During the previous segment, our generate project utility was used to scaffold the check-in data access library, the check-in feature shell library, the desktop check-in app, and a mobile variation of it. Once the wiring was complete, we took stock of the number of steps the tool had taken care of.
Now, we move on to building the seatmap data access library, which relies on NgRx for feature state. After that, we develop the seat listing feature library and wire it into all applications via routing. The shared buttons UI library and the shared formatting utilities library round out this portion, both of which find a home in the seat listing component.
Library for seatmap data access
Each feature in the shared seatmap area gets its own dedicated data access library. Data services and any application state management tied to the seatmap domain belong in this spot.
npm run generate-project -- library data-access --scope=seatmap --grouping-folder=shared/seatmap --npm-scope=nrwl-airlines --with-state
# or
yarn generate-project library data-access --scope=seatmap --grouping-folder=shared/seatmap --npm-scope=nrwl-airlines --with-state
At this stage, we’ll add the feature store and effects through the --with-state flag on the project generator. It’s worth mentioning that the grouping folder is set to shared/seatmap, keeping things nested.
// seatmap-data-access.module.ts
import { NgModule } from '@angular/core';
import { EffectsModule } from '@ngrx/effects';
import { StoreModule } from '@ngrx/store';
import { SeatmapEffects } from './+state/seatmap.effects';
import * as fromSeatmap from './+state/seatmap.reducer';
@NgModule({
imports: [StoreModule.forFeature(fromSeatmap.seatmapFeatureKey, fromSeatmap.reducer), EffectsModule.forFeature([SeatmapEffects])],
})
export class SeatmapDataAccessModule {}
Want an overview of everything the seatmap data access Angular module has configured? Start with the seatmap data access library itself. That's your best entry point.
ng run seatmap-data-access:lint
ng run seatmap-data-access:test --watch=false
All systems are now fully configured and operational.
Seat listing feature library
This marks the beginning of the seatmap domain's first feature—a capability that both the check-in and booking applications will rely on.
npm run generate-project -- library feature feature-seat-listing --scope=seatmap --grouping-folder=shared/seatmap --npm-scope=nrwl-airlines
# or
yarn generate-project library feature feature-seat-listing --scope=seatmap --grouping-folder=shared/seatmap --npm-scope=nrwl-airlines
With this command, Angular CLI takes care of creating both the module and the component automatically.
Now, for our feature to work in each app, a new route needs to be registered inside every feature shell module.
// check-in-feature-shell.module.ts
import { CommonModule } from '@angular/common';
import { NgModule } from '@angular/core';
import { RouterModule, Routes } from '@angular/router';
import { CheckInDataAccessModule } from '@nrwl-airlines/check-in/data-access';
import { SharedDataAccessModule } from '@nrwl-airlines/shared/data-access';
import { ShellComponent } from './shell/shell.component';
const routes: Routes = [
{
path: '',
component: ShellComponent,
children: [
{
path: '',
pathMatch: 'full',
redirectTo: 'seatmap', // 👈
},
{
path: 'seatmap', // 👈
loadChildren: () => import('@nrwl-airlines/seatmap/feature-seat-listing').then((esModule) => esModule.SeatmapFeatureSeatListingModule),
},
],
},
];
@NgModule({
declarations: [ShellComponent],
exports: [RouterModule],
imports: [RouterModule.forRoot(routes), SharedDataAccessModule, CheckInDataAccessModule, CommonModule],
})
export class CheckInFeatureShellModule {}
Since the check-in apps currently lack additional functionality, the seatmap will serve as the default route.
// booking-feature-shell.module.ts
import { CommonModule } from '@angular/common';
import { NgModule } from '@angular/core';
import { RouterModule, Routes } from '@angular/router';
import { BookingDataAccessModule } from '@nrwl-airlines/booking/data-access';
import { SharedDataAccessModule } from '@nrwl-airlines/shared/data-access';
import { ShellComponent } from './shell/shell.component';
const routes: Routes = [
{
path: '',
component: ShellComponent,
children: [
{
path: '',
pathMatch: 'full',
redirectTo: 'flight-search',
},
{
path: 'flight-search',
loadChildren: () => import('@nrwl-airlines/booking/feature-flight-search').then((esModule) => esModule.BookingFeatureFlightSearchModule),
},
{
path: 'passenger-info',
loadChildren: () => import('@nrwl-airlines/booking/feature-passenger-info').then((esModule) => esModule.BookingFeaturePassengerInfoModule),
},
{
path: 'seatmap', // 👈
loadChildren: () => import('@nrwl-airlines/seatmap/feature-seat-listing').then((esModule) => esModule.SeatmapFeatureSeatListingModule),
},
],
},
];
@NgModule({
declarations: [ShellComponent],
exports: [RouterModule],
imports: [RouterModule.forRoot(routes), SharedDataAccessModule, BookingDataAccessModule, CommonModule],
})
export class BookingFeatureShellModule {}
Within the booking application, the flight search remains the default landing route.
Watch out! Prior to attempting any navigation to a seatmap route, route configuration within the seat listing feature must be established first.
// seatmap-feature-seat-listing.module.ts
import { CommonModule } from '@angular/common';
import { NgModule } from '@angular/core';
import { RouterModule, Routes } from '@angular/router';
import { SeatListingComponent } from './seat-listing/seat-listing.component';
const routes: Routes = [
// 👈
{
path: '',
pathMatch: 'full',
component: SeatListingComponent,
},
];
@NgModule({
declarations: [SeatListingComponent],
imports: [
RouterModule.forChild(routes), // 👈
CommonModule,
],
})
export class SeatmapFeatureSeatListingModule {}
To wrap things up, the seatmap data access Angular module gets registered.
// seatmap-feature-seat-listing.module.ts
import { CommonModule } from '@angular/common';
import { NgModule } from '@angular/core';
import { RouterModule, Routes } from '@angular/router';
import { SeatmapDataAccessModule } from '@nrwl-airlines/seatmap/data-access';
import { SeatListingComponent } from './seat-listing/seat-listing.component';
const routes: Routes = [
{
path: '',
pathMatch: 'full',
component: SeatListingComponent,
},
];
@NgModule({
declarations: [SeatListingComponent],
imports: [
RouterModule.forChild(routes),
SeatmapDataAccessModule, // 👈
CommonModule,
],
})
export class SeatmapFeatureSeatListingModule {}
Launch the mobile check-in app and verify that it runs without any errors.
ng run check-in-mobile:serve
In the browser, the title should read check-in-mobile, and the displayed text should be seat-listing works!.
The seat listing feature Angular module bears a strong resemblance to a feature shell Angular module. That similarity exists because the seat listing component serves as the main entry point for the seatmap domain. Still, this Angular module is loaded lazily to avoid fetching it unless the user actually needs it.
Shared buttons UI library
Now we’ll build our first set of reusable presentational components and make them available through a dedicated shared buttons UI library.
npm run generate-project -- library ui ui-buttons --scope=shared --npm-scope=nrwl-airlines
# or
yarn generate-project library ui ui-buttons --scope=shared --npm-scope=nrwl-airlines
We’ll remove the boilerplate component and scaffold a fresh confirm button using a SCAM pattern instead.
npx rimraf libs/shared/ui-buttons/src/lib/buttons
ng generate module confirm-button --project=shared-ui-buttons
ng generate component confirm-button --project=shared-ui-buttons --export --display-block
The confirm button is given a straightforward implementation in the listings below.
<!-- confirm-button.component.html -->
<button (click)="onClick()">
<ng-content></ng-content>
</button>
// confirm-button.component.ts
import { Component, EventEmitter, Input, Output } from '@angular/core';
@Component({
selector: 'nrwl-airlines-confirm-button',
styleUrls: ['./confirm-button.component.css'],
templateUrl: './confirm-button.component.html',
})
export class ConfirmButtonComponent {
@Input()
message = 'Do you confirm this action?';
@Output()
confirmed = new EventEmitter<boolean>();
onClick() {
this.confirmed.emit(confirm(this.message));
}
}
Open the shared UI buttons module and adjust its exports so that only the SCAM for the confirm button is exposed.
// shared-ui-buttons.module.ts
import { NgModule } from '@angular/core';
import { ConfirmButtonModule } from './confirm-button/confirm-button.module';
@NgModule({
exports: [
// 👈
ConfirmButtonModule, // 👈
],
})
export class SharedUiButtonsModule {}
As a final step, ensure the confirm button component class is exported through the library's public API. This enables consumers to retain a reference to an instance or to render a confirm button dynamically.
// libs/shared/ui-buttons/src/index.ts
/*
* Public API Surface of shared-ui-buttons
*/
export * from './lib/shared-ui-buttons.module';
export * from './lib/confirm-button/confirm-button.component'; // 👈
The confirm button appears in the seat listing component for now, but the exact same approach applies to any other domain you might use it in.
// seatmap-feature-seat-listing.module.ts
import { CommonModule } from '@angular/common';
import { NgModule } from '@angular/core';
import { RouterModule, Routes } from '@angular/router';
import { SeatmapDataAccessModule } from '@nrwl-airlines/seatmap/data-access';
import { SharedUiButtonsModule } from '@nrwl-airlines/shared/ui-buttons'; // 👈
import { SeatListingComponent } from './seat-listing/seat-listing.component';
const routes: Routes = [
{
path: '',
pathMatch: 'full',
component: SeatListingComponent,
},
];
@NgModule({
declarations: [SeatListingComponent],
imports: [
RouterModule.forChild(routes),
SeatmapDataAccessModule,
CommonModule,
SharedUiButtonsModule, // 👈
],
})
export class SeatmapFeatureSeatListingModule {}
Because the seat listing component's declaring module is the seat listing feature module (as covered in the preceding listing), that is where you need to register it initially.
With that import in place, the seat listing component's compilation scope now includes the store, so its template can reference it and link it to the component's state.
<!-- seat-listing.component.html -->
<p>seat-listing works!</p>
<nrwl-airlines-confirm-button message="Do you confirm checking in at this seat?" (confirmed)="onSeatConfirmed($event)"> Check in </nrwl-airlines-confirm-button>
// seat-listing.component.ts
import { Component } from '@angular/core';
@Component({
selector: 'seatmap-seat-listing',
styleUrls: ['./seat-listing.component.css'],
templateUrl: './seat-listing.component.html',
})
export class SeatListingComponent {
onSeatConfirmed(isConfirmed: boolean): void {
// 👈
console.log('Is seat confirmed?', isConfirmed);
}
}
In the code snippets above, we send a message to the confirmation dialog and then subscribe to the user's choice, outputting that result to the browser's console.
Shared formatting utilities library
The last library we build in this workspace is dedicated to formatting utilities shared across the project.
npm run generate-project -- library util util-formatting --scope=shared --npm-scope=nrwl-airlines
# or
yarn generate-project library util util-formatting --scope=shared --npm-scope=nrwl-airlines
At this stage, the library’s only responsibility is to surface one pure function. Remove the generated Angular module along with its corresponding tests.
npx rimraf libs/shared/util-formatting/src/lib/*.module*.ts
For handling dates and times in this project, Luxon is the library we're going with.
npm install luxon
npm install --save-dev @types/luxon
# or
yarn add luxon
yarn add --dev @types/luxon
Inside the library's lib directory, add a new file with the name format-date.ts.
// format-date.ts
import { DateTime } from 'luxon';
export function formatDate(luxonDate: DateTime): string {
return luxonDate.toLocaleString({ ...DateTime.DATE_MED, weekday: 'long' });
}
Add it to the library's public API, and don't forget to drop the exported Angular module we removed.
// libs/shared/util-formatting/src/index.ts
/*
* Public API Surface of shared-util-formatting
*/
export * from './lib/format-date'; // 👈
Now we can apply the same formatting helper inside the seat listing component.
// seat-listing.component.ts
import { Component } from '@angular/core';
import { formatDate } from '@nrwl-airlines/shared/util-formatting'; // 👈
import { DateTime } from 'luxon'; // 👈
@Component({
selector: 'seatmap-seat-listing',
styleUrls: ['./seat-listing.component.css'],
templateUrl: './seat-listing.component.html',
})
export class SeatListingComponent {
get today(): string {
// 👈
const now = DateTime.local();
return formatDate(now);
}
onSeatConfirmed(isConfirmed: boolean): void {
console.log('Is seat confirmed?', isConfirmed);
}
}
With the state in place, it’s time to surface the UI property inside the seat listing template.
<!-- seat-listing.component.html -->
<p>Today is {{ today }}<!-- ? --></p>
<nrwl-airlines-confirm-button message="Do you confirm checking in at this seat?" (confirmed)="onSeatConfirmed($event)"> Check in </nrwl-airlines-confirm-button>
Calling date and time methods straight inside a declarable is considered bad practice — it breaks determinism and complicates testing. A dedicated service should have been the approach for date-time retrieval. Feel free to implement that on your own.
Since the focus here was merely on establishing a formatting utilities library, making date-time access an abstracted service remains a task for you to tackle independently.
Here’s how the shared formatting utilities library’s file and folder arrangement ends up:
libs/shared/util-formatting
├── src
│ ├── lib
│ │ └── format-date.ts
│ ├── index.ts
│ └── test.ts
├── README.md
├── karma.conf.js
├── tsconfig.lib.json
├── tsconfig.spec.json
└── tslint.json
Conclusion
Launch the desktop check-in app using ng run check-in-desktop:serve. This should produce a UI matching the screenshot shown below.
Great work! As shown below, the Nrwl Airlines monorepo workspace is now fully equipped with multiple apps alongside workspace libraries.
nrwl-airlines
├── apps
│ ├── booking
│ │ ├── booking-desktop
│ │ ├── booking-desktop-e2e
│ │ ├── booking-mobile
│ │ └── booking-mobile-e2e
│ └── check-in
│ ├── check-in-desktop
│ ├── check-in-desktop-e2e
│ ├── check-in-mobile
│ └── check-in-mobile-e2e
├── libs
│ ├── booking
│ │ ├── data-access
│ │ ├── feature-flight-search
│ │ ├── feature-passenger-info
│ │ └── feature-shell
│ ├── check-in
│ │ ├── data-access
│ │ └── feature-shell
│ └── shared
│ ├── data-access
│ ├── environments
│ ├── seatmap
│ │ ├── data-access
│ │ └── feature-seat-listing
│ ├── ui-buttons
│ └── util-formatting
└── tools
At the end of this series, we first created the seatmap data access library bundled with feature state.
After that, we scaffolded the seat listing feature library, then wired seatmap routing into the check-in and booking feature shell Angular modules. For that to function, we inserted one route pointing to the seat listing component inside the seatmap listing feature shell Angular module.
To complete the seatmap domain, we wired seatmap data access into the seat listing feature Angular module, which serves as the domain's primary feature library.
We scaffolded the shared buttons UI library, then built and exported the confirm button component. That component displayed a check-in confirmation dialog within the seat listing component.
Last, we created the shared formatting utilities workspace library, adding the format date function. The seat listing component used it to render the current date, matching the screenshot found in this closing section.
Tutorial series conclusion
This tutorial series showed how to generate an Nx-style workspace with the Angular CLI. We relied on default schematics, adapted them, and finally scripted those changes with a custom Node.js command-line tool. Converting that tool into Angular schematics would be beneficial, though it falls outside what this article covers.
View the generate project tool at GitHub Gists.
The generate project tool could have used the programmatic APIs of the other command-line tools we employed, but keeping the calls explicit makes it simpler to see how the manually entered commands map to the tool's behavior.
We built application projects containing minimal logic. We produced small workspace libraries that either encapsulate logic tied to a specific use case or hold reusable functionality.
Thanks to path mappings, both application and library projects can reference library projects using the import path prefix we chose—the --npm-scope value provided to the generate project tool.
A monorepo workspace structure and its commands don't require the Nx CLI. Nx CLI shares the same base components as the Angular CLI, including Schematics, builders, and workspace configuration.
What's missing?
Nx CLI encompasses more than just schematics. It enforces architectural boundaries, preventing dependencies between layers we choose to isolate. Building that ourselves, for instance with the TSLint import-blacklist rule, would be both tedious and error-prone.
Nx CLI lets us generate a dependency graph that shows how our projects are interconnected, aiding our reasoning about those links. A similar result could come from Dependency cruiser.
Nx also provides schematics for additional frameworks and tools, such as ESLint, Jest, Cypress, Storybook, React, Express, and Nest. There's no direct replacement for most, though Nest ships its own schematics and Storybook has a generator-like command.
Nx introduces many commands, utilities, and settings that simplify creating a production-ready deployment pipeline—incremental builds, distributed cache, affected builders, and parallel execution are just some. Some of those could be assembled using other tools and setup, but when available, the Nx CLI won't let you down.
Resources
Check the GitHub repository LayZeeDK/ngx-nrwl-airlines-workspace for the complete solution.
Peer reviewers
I appreciate the professionals who helped refine this tutorial:

