Setting Up an Nx Workspace with an Angular Application
To illustrate this approach, we'll start by generating an Nx workspace containing a single Angular application. Run the commands shown in Listing 1.
npx create-nx-workspace workspace --cli=angular --preset=angular --appName=tiny-app --style=scss
nx update @angular/cli @angular/core
We'll then create workspace libraries that the application can access through the @workspace scope.
Extracting an Assets Workspace Library
When an Angular application is generated, it includes an empty assets directory intended for static resources like icons, images, and web fonts. These assets can be referenced from DOM attributes and stylesheets using absolute paths, such as <img src="/assets/images/logo.png" /> or .twitter { background-image: url('/assets/icons/twitter.png'); }.
Generated Angular applications also include a static favicon.ico file, which is referenced in index.html. Our plan is to generate a dedicated assets workspace library, move the static files there, adjust the workspace configuration, and update all references to point to the new library.
Generating a Clean Workspace Library
The initial step involves creating a workspace library and stripping it down, since it will only hold static files rather than TypeScript code.
nx generate library assets --directory=shared --tags="scope:shared,type:assets" --style=scss
npx rimraf ./apps/tiny-app/src/assets ./libs/shared/assets/*.js ./libs/shared/assets/*.json ./libs/shared/assets/src/*.* ./libs/shared/assets/src/lib
"# shared-assets" > ./libs/shared/assets/README.md
After running the commands from Listing 2, modify angular.json so that all architect targets are removed from the shared-assets project, resulting in a structure matching Listing 3.
{
"//": "angular.json",
"projects": {
"shared-assets": {
"architect": {}
}
}
}
Creating Common Assets Folders and Relocating the Favicon
With the clean workspace library structure in place, create the standard asset folders and move the favicon into the new assets library by executing the commands in Listing 4.
npx mkdirp ./libs/shared/assets/src/assets/fonts ./libs/shared/assets/src/assets/icons ./libs/shared/assets/src/assets/images
"" > ./libs/shared/assets/src/assets/fonts/.gitkeep
"" > ./libs/shared/assets/src/assets/icons/.gitkeep
"" > ./libs/shared/assets/src/assets/images/.gitkeep
mv ./apps/tiny-app/src/favicon.ico ./libs/shared/assets/src
To make the Angular application use the assets from the workspace library, open the tiny-app:build architect target in angular.json and swap out the assets option with the configuration shown in Listing 5.
{
"//": "angular.json",
"projects": {
"tiny-app": {
"architect": {
"build": {
"options": {
"assets": [
{
"glob": "favicon.ico",
"input": "libs/shared/assets/src",
"output": "./"
},
{
"glob": "**/*",
"input": "libs/shared/assets/src/assets",
"output": "assets"
}
]
}
}
}
}
}
}
This tells the Angular CLI to copy the favicon to the dist/apps/tiny-app directory during the build. Additionally, every file and subfolder under libs/shared/assets/src/assets gets copied to dist/apps/tiny-app/assets. This ensures asset links keep working in non-local environments, such as staging or production servers.
Verifying Locally
Feel free to test this locally by running nx serve --open on the Webpack development server. Alternatively, run the commands in Listing 6 to create a production bundle and serve it with a local static file server. In both cases, confirm the favicon appears correctly.
nx build --prod
npx http-server dist/apps/tiny-app -o
Bundling an Asset
Nx-generated Angular applications display an Nx logo in the app component, as shown at the top of Figure 1.
If you inspect app.component.html, you'll notice the logo is sourced from https://nx.dev/assets/images/nx-logo-white.svg.
We'll incorporate this logo into our application bundle by adding it to the assets library and updating the image source in the app component's template.
Run the command in Listing 7 to fetch the Nx logo and save it within the assets library.
npx -p wget-improved nwget https://nx.dev/assets/images/nx-logo-white.svg -O ./libs/shared/assets/src/assets/images/nx-logo-white.svg
Next, edit app.component.html to point the image element to the logo from our assets library, as demonstrated in Listing 8.
<!-- app.component.html -->
<img
alt="Nx logo"
width="75"
src="/assets/images/nx-logo-white.svg"
/>
That covers it. We've successfully extracted an assets workspace library and included static files in the bundle. Give it another run to confirm everything is working as expected.
Extracting a Styles Workspace Library
Angular applications come with a global stylesheet named styles.css, or in our case styles.scss since we're using Sass. This file typically contains generic, element-level, object, and utility styles.
As an app grows, its global stylesheet tends to become larger and more complex. With Sass, we have the option to break down a stylesheet into partials, which by convention are prefixed with an underscore (_), like _global.scss.
Sass partials are combined via import statements, for instance @import './lib/global';. It's worth noting that Sass follows naming conventions to locate files regardless of whether they carry the underscore prefix.
Unlike plain CSS, Sass import statements are not loaded one by one or asynchronously—at least not when referencing our static assets. They're instead merged into one single stylesheet. This behavior parallels how tools such as Webpack or Browserify bundle JavaScript and TypeScript files.
To slim down our Angular application project, we'll create a styles workspace library, turn styles.scss into a Sass partial, incorporate it into a library stylesheet that serves as an entry point, and update the app's configuration to reference this new stylesheet.
Generating a Clean Workspace Library
Similar to earlier, we begin by generating a workspace library and cleaning it up, as it will contain only stylesheets, not TypeScript code.
nx generate library styles --directory=shared --tags="scope:shared,type:styles" --style=scss
npx rimraf ./libs/shared/styles/*.js ./libs/shared/styles/*.json ./libs/shared/styles/src/*.* ./libs/shared/styles/src/lib/*.*
"# shared-styles" > ./libs/shared/styles/README.md
Once the commands from Listing 9 are executed, modify angular.json to remove all architect targets from the shared-styles project, aligning with the setup in Listing 10.
{
"//": "angular.json",
"projects": {
"shared-styles": {
"architect": {}
}
}
}
Creating an Entry Point Stylesheet
With the workspace folder structure cleaned up, we're ready to build an index.scss file that acts as the entry point for our styles workspace library.
At the same time, we'll convert the application's stylesheet (styles.scss) into a Sass partial by renaming it and relocating it into the styles library. This is carried out using the commands in Listing 11.
mv ./apps/tiny-app/src/styles.scss ./libs/shared/styles/src/lib/_global.scss
"@import './lib/global';" > ./libs/shared/styles/src/index.scss
There's just one final adjustment: edit angular.json so the styles option of the tiny-app:build architect target points to the entry as indicated in Listing 12A.
{
"//": "angular.json",
"projects": {
"tiny-app": {
"architect": {
"build": {
"options": {
"styles": [
"libs/shared/styles/src/index.scss"
]
}
}
}
}
}
}
Keep in mind that if you're using Karma and have component tests that depend on global styles, you'll need to add a comparable option to the test architect target for your UI workspace libraries, as illustrated in Listing 12B.
{
"//": "angular.json",
"projects": {
"ui-buttons": {
"architect": {
"test": {
"builder": "@angular-devkit/build-angular:karma",
"options": {
"styles": [
"libs/shared/styles/src/index.scss"
]
}
}
}
}
}
}
If a UI library is used across multiple applications and its tests rely on their respective global styles, you'd need to set up multiple test configurations for that project, as shown in Listing 12C.
{
"//": "angular.json",
"projects": {
"ui-buttons": {
"architect": {
"test": {
"builder": "@angular-devkit/build-angular:karma",
"configuration": {
"booking": {
"styles": [
"libs/booking/shared/styles/src/index.scss"
]
},
"check-in": {
"styles": [
"libs/check-in/shared/styles/src/index.scss"
]
}
}
}
}
}
}
}
Verifying Locally
Angular CLI now links index.scss in index.html, both on the local development server and in deployed environments where the stylesheet is bundled with the application.
Be sure to give it a test—add some global styles and see if they're applied correctly.
nx build --prod
npx http-server dist/apps/tiny-app -o
Use nx serve --open to check global styles locally, or execute the commands from Listing 6 to serve a production build on a local static file server.
Move environment configuration into a workspace library
Before the Angular application is bootstrapped in main.ts, the code conditionally invokes enableProdMode. This decision hinges on the production Boolean property present in the environment object.
Activating enableProdMode suppresses an additional runtime change detection cycle that otherwise runs in development mode. That extra cycle is responsible for surfacing the ExpressionChangedAfterItHasBeenCheckedError warning.
Development mode also enables extra runtime assertions within Angular's core code paths.
Scaffold a new workspace library
Our intended workspace library will be small and narrowly scoped, but because it contains TypeScript, the lint and test architect targets remain valuable additions.
nx generate library environments --directory=shared --tags="scope:shared,type:environments" --style=scss
npx rimraf ./libs/shared/environments/src/lib/*.*
The commands in Listing 13 first generate the environments library. After that, we delete the default files that were scaffolded into the library's src/lib directory.
Relocate environment files and wire up dependencies
With an empty lib folder prepared inside the environments library, we can shift the environment files from the application project. We then make them accessible via the library's public entry point and finally remove the environments directory from the application. Listing 14 contains the exact commands for this sequence.
mv ./apps/tiny-app/src/environments/*.* ./libs/shared/environments/src/lib
"export * from './lib/environment';" > ./libs/shared/environments/src/index.ts
npx rimraf ./apps/tiny-app/src/environments
To point the Angular application at the library-based environment file according to its build configuration, we open angular.json and locate the tiny-app:build architect target. Inside the production configuration, we swap the existing fileReplacements entry with the one shown in Listing 15.
{
"//": "angular.json",
"projects": {
"tiny-app": {
"architect": {
"build": {
"configurations": {
"production": {
"fileReplacements": [
{
"replace": "libs/shared/environments/src/lib/environment.ts",
"with": "libs/shared/environments/src/lib/environment.prod.ts"
}
]
}
}
}
}
}
}
}
One final adjustment remains. The import statement found in main.ts must be updated to reference the newly created environments workspace library, as demonstrated in Listing 16.
// main.ts
import { enableProdMode } from '@angular/core';
import { environment } from '@workspace/shared/environments';
if (environment.production) {
enableProdMode();
}
Verify the changes locally
The Angular CLI will now substitute environment.ts with environment.prod.ts when assembling the production bundle, even though the application project only holds a transitive dependency on environment.ts.
It's worth testing this locally. After running nx serve --open, check the browser's developer console. You should see the message Angular is running in the development mode. Call enableProdMode() to enable the production mode.
nx build --prod
npx http-server dist/apps/tiny-app -o
When you serve a production build locally with the commands listed in Listing 6, no console output should appear in the browser.
Introduce compile-time configuration for libraries
The environments library offers a way to configure application dependencies because it exposes environment settings that can be referenced during compile-time configuration tasks.
In typical setups, we might provide an environment service that modules, components, or services can inject. However, injection isn't viable for methods that generate ModuleWithProviders<T> objects, such as static forRoot methods on Angular modules.
Angular module imports face a similar constraint. Suppose we want certain modules loaded only in development mode but excluded from production bundles. Relying on an injected environment value won't work here because the decision happens at compile time, requiring static access to a constant.
Having a workspace library directly depend on an application project would be a poor architectural choice. Such a dependency reverses the expected direction of dependencies and risks creating circular references.
Install and set up NgRx Store
To illustrate the approach, we'll integrate NgRx Store along with its development tooling by leveraging the ng add schematics, as shown in Listing 17.
nx add @ngrx/store --minimal false
nx add @ngrx/store-devtools
We'll relocate the NgRx Store setup from AppModule into CoreModule. This is the conventional location for configuring the root injector in classic Angular projects. CoreModule gets imported by AppModule and its contents are displayed in Listing 18.
// core.module.ts
import { NgModule } from '@angular/core';
import { StoreModule } from '@ngrx/store';
import { StoreDevtoolsModule } from '@ngrx/store-devtools';
import { environment } from '@workspace/shared/environments';
import { metaReducers, reducers } from './reducers';
@NgModule({
imports: [
StoreModule.forRoot(reducers, {
metaReducers,
}),
StoreDevtoolsModule.instrument({
logOnly: environment.production,
maxAge: 25,
}),
],
})
export class CoreModule {}
That arrangement works fine in a standard Angular workspace. Our goal, though, is to keep the application project as lean as possible by moving logic out of it.
Create a shared data access library
We aim to keep all NgRx-specific root injector configuration inside a workspace library. Nx designates a dedicated library type for data access, so we'll generate one and relocate the configuration logic into it.
nx generate library data-access --directory=shared --tags="scope:shared,type:data-access" --style=scss
mv ./apps/tiny-app/src/app/reducers ./libs/shared/data-access/src/lib
Run the commands in Listing 19 to create the shared data access library and move the src/app/reducers folder that was added when NgRx Store was originally installed.
Open libs/shared/data-access/src/lib/shared-data-access.module.ts and replace its contents with the code in Listing 20.
// shared-data-access.module.ts
import { ModuleWithProviders, NgModule } from '@angular/core';
import { StoreModule } from '@ngrx/store';
import { StoreDevtoolsModule } from '@ngrx/store-devtools';
import { environment } from '@workspace/shared/environments';
import { metaReducers, reducers } from './reducers';
@NgModule({
imports: [
StoreModule.forRoot(reducers, {
metaReducers,
}),
StoreDevtoolsModule.instrument({
logOnly: environment.production,
maxAge: 25,
}),
],
})
export class SharedDataAccessRootModule {}
@NgModule({})
export class SharedDataAccessModule {
static forRoot(): ModuleWithProviders<SharedDataAccessRootModule> {
return {
ngModule: SharedDataAccessRootModule,
};
}
}
We adopt the forRoot pattern, a convention which signals that any dependencies supplied when importing this module are intended for the root injector. That's implemented via a static method returning a ModuleWithProviders<T> instance.
The SharedDataAccessRootModule referenced in that module-with-providers object carries the configuration that previously lived inside CoreModule.
Finally, navigate to apps/tiny-app/src/app/core.module.ts and change its file content to what's shown in Listing 21.
// core.module.ts
import { NgModule } from '@angular/core';
import { SharedDataAccessModule } from '@workspace/shared/data-access';
@NgModule({
imports: [
SharedDataAccessModule.forRoot(),
],
})
export class CoreModule {}
After this restructuring, the workspace dependency graph takes the form illustrated in Figure 2.
Had we not isolated the environments configuration into its own library, importing an environment file from our shared data access library would have been impossible. The tiny-app project lacks a scoped path mapping, and more fundamentally, a library project must never establish a dependency on an application project.
Register a development-only meta reducer
The environment object can now serve when configuring injectors. The generated NgRx Store configuration has another integration point, namely the reducers file itself, where meta reducers are declared. That setup is visible in Listing 22.
// reducers/index.ts
import { ActionReducerMap, MetaReducer } from '@ngrx/store';
import { environment } from '@workspace/shared/environments';
export interface State {}
export const reducers: ActionReducerMap<State> = {};
export const metaReducers: MetaReducer<State>[] =
!environment.production ? [] : [];
Let's incorporate a technique from the NgRx documentation by adding a debug meta reducer that only runs in development.
// reducers/debug.ts
import { ActionReducer } from '@ngrx/store';
export function debug(reducer: ActionReducer<any>): ActionReducer<any> {
return (state, action) => {
console.log('state', state);
console.log('action', action);
return reducer(state, action);
};
}
The debug meta reducer from Listing 23 outputs the current NgRx Store state along with the dispatched action name prior to the state reduction step, on every action dispatch.
// reducers/index.ts
import { ActionReducerMap, MetaReducer } from '@ngrx/store';
import { environment } from '@workspace/shared/environments';
import { debug } from './debug';
export interface State {}
export const reducers: ActionReducerMap<State> = {};
export const metaReducers: MetaReducer<State>[] =
!environment.production ? [debug] : [];
Listing 24 demonstrates how to include that debug meta reducer exclusively for development mode builds. Note that the environment object is now imported from the environments workspace library.
// shared-data-access.module.ts
import { NgModule } from '@angular/core';
import { StoreModule } from '@ngrx/store';
import { metaReducers, reducers } from './reducers';
@NgModule({
imports: [
StoreModule.forRoot(reducers, {
metaReducers,
}),
],
})
export class SharedDataAccessRootModule {}
The exported metaReducers array is then consumed when configuring the root store, as seen in Listing 25.
Figure 3 illustrates the file and folder layout for the shared data access library, which contains both the root store configuration and the meta reducer definitions.
Set up Nx workspace dependency rules
Nx projects come with a workspace-wide configuration that allows you to define restrictions for internal dependencies while also communicating dependencies that aren't evident from TypeScript imports.
{
"//": "nx.json",
"projects": {
"tiny-app": {
"implicitDependencies": [
"shared-assets",
"shared-styles"
]
}
}
}
Listing 25 demonstrates how to declare implicit dependencies from the application project to the assets and styles libraries. These dependencies are required because no TypeScript code in the application explicitly imports those libraries.
The environments library, however, is directly imported in main.ts, making its dependency explicit and automatically detectable by Nx.
Defining these dependencies ensures Nx's affected:* commands recognize modifications made to the assets or styles libraries.
When running nx affected:build, that recognition forces a rebuild of the application project. Likewise, nx affected:test or nx affected:e2e will trigger the corresponding test suites, and nx affected:dep-graph will highlight all projects modified or impacted by the change.
If we edit _global.scss and then execute nx affected:dep-graph, the resulting graph is shown in Figure 4 with the affected project nodes emphasized.
Building a minimal Angular application project
Once the workspace has been reorganized, the dependency graph forms a directed acyclic graph (DAG) where all edges point in the appropriate direction, as illustrated in Figure 5.
The end-to-end test project tiny-app-e2e has a dependency on the application project. Consequently, any modification to the application project triggers an impact on the e2e tests, requiring them to be executed again.
The tiny-app application project depends on the shared workspace libraries shared-environments, shared-assets, and shared-styles. A change in any of these libraries necessitates a rebuild of the application along with a fresh run of its test suites. Figure 2 demonstrated such a scenario when a modification was made to shared-styles.
It remains true that no workspace library has a dependency on the application project. If that ever becomes the case, we have introduced an architectural mistake.
Because the application project contains very little logic, there are only a few circumstances under which it needs to be modified. In practice, the application project should rarely be touched again.
When reviewing pull requests, the extent of the changes can be quickly assessed by checking which workspace library folders contain modified files, or by executing nx affected:dep-graph as demonstrated earlier in this article.
The standard file and folder layout that Nx generates for an Angular application is shown in Figure 6. Configuration files such as tsconfig.json and tslint.json are omitted from the diagram because they are not affected by the techniques covered here.
In the tiny app project, the files within the src/app subfolder remain identical to the default application project, with one exception: a CoreModule was introduced in core.module.ts when the shared data access library was created.
Figure 7 shows that every subfolder under src has been relocated, with the sole exception of src/app.
The shared assets workspace library
As shown in Figure 8, the assets folder has been relocated from the application project into the shared-assets workspace library.
We established the common asset directories fonts, icons, and images, and included the Nx logo as evidenced in the src/assets/images subfolder within the assets library.
The .gitkeep entries serve as empty placeholders that preserve the directory hierarchy in the Git repository even when the directories contain no actual files. Once real files are added and tracked under version control, these placeholder files can be removed. For instance, deleting src/assets/images/.gitkeep is now appropriate since nx-logo-white.svg has been added to that same directory.
In a typical application project, the favicon resides in the src subfolder. We also transferred this file to the assets library, placing it in the corresponding src subfolder.
Glob patterns defined in the tiny-app:build architect target within angular.json guarantee that files from the assets workspace library are included when the application is bundled during the build process.
Since this library consists solely of static files, no TypeScript configuration files are present.
The shared styles workspace library
The global stylesheet styles.scss has been moved from the src subfolder of the application project into the shared-styles workspace library, as depicted in Figure 9.
styles.scss has been renamed to _global.scss, transforming it into a Sass partial. This partial is stored in the src/lib subfolder within the styles library. It gets imported by the entry point stylesheet index.scss located in the src subfolder.
The library does not contain any TypeScript configuration files since it exclusively holds stylesheets and Sass partials.
The shared environments workspace library
The environment files have been relocated from the application project's src/environments subfolder to the src/lib subfolder inside our environments workspace library, as illustrated in Figure 10.
The environments library re-exports the environment object through its entry point, which serves as the public API and is declared in index.ts.
Because this workspace library contains TypeScript code, it retains its TypeScript, TSLint, and Jest configuration files, along with the lint and test architect targets.
Wrapping up
We started by generating an Nx workspace that includes a single Angular application. Even prior to introducing any features, it is possible to extract workspace libraries in order to comply with the Single Responsibility Principle.
The assets library
The shared assets workspace library holds static content including web fonts, icons, and images, as well as the favicon. The web app manifest would also belong in this library.
We demonstrated how an image file can be added to the library and referenced from the application project. Naturally, this approach also works from UI workspace libraries and feature libraries.
By keeping static files in a dedicated workspace library, we reduce the likelihood of inadvertently breaking the entire application when adding, removing, or editing static resources.
The styles library
Having a workspace library dedicated exclusively to global styles means we no longer have to worry about cluttering the application project with numerous Sass partials or accidentally disrupting the application configuration.
The shared styles workspace library can additionally expose Sass mixins, functions, and partials that are reused across component styles or UI workspace libraries.
The environments library
Moving the environment files into a shared workspace library enables conditional configuration of injectors from workspace libraries, such as the shared data access library we created to set up NgRx Store in the root injector.
In a production application, we might introduce a feature shell library that serves as the orchestration Angular module imported by either AppModule or CoreModule.
Without a feature shell library, any additional configuration of the root injector or new application use cases requires modifying the application project. This introduces risk. It is far safer to keep the application project largely untouched under normal circumstances for greater confidence.
Shared workspace libraries
In the examples throughout this article, we placed the extracted workspace libraries under the shared grouping folder and assigned the scope:shared tag. For a workspace that contains only a single application, this level of structure may be unnecessary.
Still, as the application expands, we will be glad that grouping folders were used from the outset. Application-wide workspace libraries live under the shared grouping folder, while sub-domain grouping folders are used to organize feature libraries alongside their associated data access, domain, and UI workspace libraries.
Without this approach, we would potentially wind up with dozens or even hundreds of library folders directly inside the libs directory, each requiring progressively longer folder names.
If we later decided to add more applications to the workspace, the workspace libraries intended for sharing across applications would remain in the shared grouping folder. Libraries that should not be shared between applications could reside in a grouping folder named after the specific application, such as libs/tiny-app/shared for application-wide libraries exclusive to the tiny-app application project.
Additional resources
You are welcome to clone LayZeeDK/nx-tiny-app-project from GitHub to explore the complete solution.
Watch a video walkthrough of this article by Oscar Lagatta.
For more on implementing a feature shell library, see "Shell Library patterns with Nx and Monorepo Architectures" by Nacho Vázquez.
Peer reviewers
Special thanks to Nacho Vazquez for his insightful feedback on this article and for the many stimulating conversations that brought us to shared architectural conclusions 🙇♂️










