DEV Community

Sergey Kolomiets
Sergey Kolomiets

Posted on

Building Scalable Enterprise Angular Applications with Nx

Introduction

Modern enterprise applications often struggle under the weight of legacy code, tightly coupled modules, and growing client demands.
Rewriting everything from scratch is risky and costly — but simply maintaining outdated systems slows innovation.

This article briefly explores how Nx and feature-oriented design can help organizations modernize step by step monolith application and be prepared
for deep customization to meet client's requirements and other architecture solutions such as micro frontends or web components.

Scenario

Imagine working for a software development company called Acme Corp, which develops a product named
Acme Case Management. This system provides out-of-the-box functionality for case management. Core features include:

  • Cases: Create and manage cases.
  • Tasks: Review, manage, and assign tasks.
  • Users: Manage system users.
  • Administration: Configure roles, permissions, system settings, etc.

The application has typical Enterprise UI with navigations, list of entities, details views, forms, etc.:

Case Management APplication UI

The main problem is that application is based on 10+ years old legacy code, and is a mix of AngularJS 1.x, jQuery,lodash, and set of libraries, that not supported anymore.The application is monolithic and tightly coupled, making it difficult to maintain and extend. The goal is to modernize the application, improve its architecture, and make it more flexible for future enhancements.

Legacy application

Business Requirements

Legacy Monolithic Application Enhancement

Acme Corp has to support a legacy monolithic application that is used by many clients.Instead of rewriting the entire legacy application from scratch for these clients, Acme Corp wants to enhance the existing monolithic application. The goal is to modernize the application by adding new features and replacing existing components with new ones, that are built using modern Angular and web components.
This approach minimizes risk for existing clients and ensures a smoother transition to a modern application. One day all outdated components will be replaced with modern Angular components.

Multiple Versions of the System

The system should have a few versions that are adapted to work in healthcare, government and other domains. Core functionality remains the same, but some features can be modified or added new ones.

Client-Specific Customizations

Customization is a key requirement for Acme Case Management. Clients should be able to combine existing components with custom ones, replace any elements, like services or components by custom ones, and remove unnecessary ones, add or modify internalization resources, etc.

To understand how different can be requirements, let's take a look on their clients requirements:

  1. Wonka Inc: Requires a new custom reports functionality and add Oompa-Loompish language support to the UI.

Wonka Case Management UI

  1. Stark Industries: Needs to modify exists task module to introduce custom functionality, let's say they want to add attachments support to tasks.

Stark Case Management UI

  1. Wayne Enterprises: Wants two distinct applications: one for cases, tasks and users management, second only for administration.

Wayne Case Management UI

Distribution

Some clients use Acme's infrastructure to build and deploy applications, but other clients have their own deployment and build processes, it can be related to security policy or other reasons. That means Acme Corp must ensure that libraries and updates are delivered to all clients smoothly and securely.

Future plans

Acme Corp leaders have also plans to make the application available as a Software as a Service (SaaS) solution, where clients can add new features to the Case Management runtime, without rebuilding the whole application. After researching micro frontend architecture seems to be the best solution for this requirement.

Requirements Summary

Let's summarize business requirements.

  1. We need to be able to build a wide variety of monolith applications based on the same core functionality;
  2. Some of the core features can be integrated as web components into the legacy or other applications;
  3. The solution should be future-proof for micro frontend architecture;
  4. We have to be able to combine OOTB components with custom components, modify existing components, and remove unnecessary ones;
  5. We need an efficient method to distribute code with minimal effort.

Solution

To meet the business requirements, we will use a Feature-Oriented Architecture. We need to split functionality into isolated reusable libraries that are focused on specific business features (Tasks, Cases, Admin, etc.). Feature libraries should encapsulate business logic that can be reused independently. Functionality that can be reused across
different features should be placed in core libraries (UI controls, Authentication, Permissions).

Let's start with creating a new Nx workspace:

npx create-nx-workspace acme --preset=angular-monorepo  --appName=acme-core --bundler=esbuild --style=scss --ssr=false --unitTestRunner=jest --e2eTestRunner=playwright --ci=skip
Enter fullscreen mode Exit fullscreen mode

This command creates a new Nx workspace with the following options:

  • --preset=angular-monorepo: Creates a new Angular monorepo workspace.
  • --appName=acme-core: The name of the main application is acme-core.
  • --bundler=esbuild: Uses esbuild as the bundler for faster builds.
  • --style=scss: Uses scss for styling.
  • --ssr=false: Disables server-side rendering.
  • --unitTestRunner=jest: Uses jest for unit testing.
  • --e2eTestRunner=playwright: Uses playwright for end-to-end testing.
  • --ci=skip: Skips continuous integration setup.

For precise tuning of Nx workspace creation, please refer to the create-nx-workspace documentation.

Project Structure

There are a few types of libraries in the project:

  1. Feature Libraries: Contain specific features of the application, such as cases, tasks, users, and admin panels.
  2. Core Libraries: Contain shared services, models, interceptors, guards, pipes, directives, and other reusable components.
  3. Theme Library: Contains themes for different purposes(contrast, gov style etc). Themes are replaceable and can be easily adapted to different projects.
  4. Web Components: For integration into legacy or third party applications.
  5. Extensions: Libraries that extend core functionality for specific domains (e.g., government, healthcare). Clients' code not included into the workspace .

Based on libraries, we will create few applications in the same workspace:

  1. Acme Core Application: The main application for Acme Corp.
  2. Acme Gov Application: A government version of the application.
  3. Acme Health Application: A healthcare version of the application.
  4. Micro Frontend Application: A micro frontend version of the Acme Case management.

Monorepo Project Structure

For external clients or legacy applications, that use Acme Case Management, we can create separate workspaces, that can contain only client-specific customizations and reuse core and feature libraries from Acme workspace as dependencies:

Structure of external projects

It is important to have naming conventions for libraries and applications.
This makes it easier to identify and manage components, services, and other resources.

  • @acme/core- - core libraries (@acme/core-ui, @acme/core-permissions).
  • @acme/feature- - feature libraries (@acme/feature-tasks, @acme/feature-dashboard).
  • @acme/theme- - themes (@acme/theme-acme, @acme/theme-gov).
  • @acme/web- - web components (@acme/web-cases).
  • @acme/gov- - government related features (@acme/gov-cases).
  • @acme/hl- - healthcare related features (@acme/hl-tasks).

Let's use 'Acm' prefix for all elements (components, services, models) that belong to basic Acme functionality.
That's how we can easily recognize OOTB features: AcmCasesListComponent, AcmTasksService, AcmSystemUser, etc.

For domain specific libraries we also use own prefixes: GovTasksList, GovOrganizationManagement, HlPatient, HlInsuranceCompany.

Same approach can be used for client-specific libraries created out of the workspace scope. For example, if we have custom libraries for "Wonka Inc" and "Stark Industries", we name them: @wonka/tasks with WnkTasksService and @stark/core-controls with StrkNavigationComponent.

Also, important to separate common functionality from feature-specific functionality. This separation ensures that features can be easily customized, extended, or removed without affecting other parts of the application. Another important aspect is to keep feature-libraries independent and avoid cross-dependencies between them.

Core Libraries

Core libraries contain shared services, models, interceptors, guards, pipes, directives, and other reusable components. These libraries are intended for use by all feature libraries. Core libraries include:

  1. auth - Authentication services, guards, and interceptors.
  2. permissions - Services for loading and checking user permissions.
  3. ui - Basic UI components, directives, and pipes that are agnostic of specific projects.
  4. acme-controls - Specialized controls tailored for Acme Corp projects.
  5. i18n - Internationalization services, pipes, and directives.

Feature Libraries

The project follows a Feature-Oriented Design approach. Each feature is implemented as a standalone library.
Feature libraries include:

  1. cases - Case processing.
  2. tasks - Task review.
  3. users - Users management.
  4. admin - Users' roles, permissions, and application settings management.

Feature libraries can be used in various combinations to meet different client requirements. They can also be extended, modified, or removed as needed. Following the principles of feature-oriented design,
features should be independent and avoid dependencies on each other.

Theme Library

Many Acme Corp clients have distinct style guidelines. To support custom theming, the theme library is maintained separately. This allows for easy adaptation of the application’s appearance according to client-specific design systems.Theme libraries contain set of SCSS files with styles and variables, that help to create a consistent look and feel across the applications for different clients.

Web Components

The web folder contains web components needed for integration into legacy applications. They are only wrappers that provide web components functionality for feature components (shadow DOM management, CSS import, events, properties, etc.).

Final Project Structure

The final structure of the project is as follows:

    ├── apps/
    │   ├─── acme-core/           # Core application for Acme Corp
    │   ├─── acme-gov/            # Government version of the application
    │   ├─── acme-health/         # Health sector version of the application
    │   └─── mf/                  # Folder for micro frontend applications
    │       ├─── host/
    │       ├─── dashboard/       
    │       ├─── cases/       
    │       └─── tasks/
    │       
    └── libs/
        ├── core/                 # Core libraries
        │   ├── auth/             # Authentication services, guards, and interceptors
        │   ├── permissions/      # Permissions services
        │   ├── ui/               # Basic UI components, directives, and pipes. Project agnostic. Can be used in any project.
        │   ├── acme-controls/    # Acme-project specific controls, specific navigation controls, document previewer etc.
        │   └── i18n/             # Internationalization components and services
        │
        ├── feature/              # Feature libraries
        │   ├── dashboard/
        │   ├── cases/
        │   ├── tasks/
        │   └── admin/
        │
        ├── theme/                # Themes
        │   ├── acme/
        │   └── acme-gov/
        │
        ├── web/                  # Web components 
        |
        ├── gov/                  # Government specific libraries
        ├── hl/                   # Health specific libraries
Enter fullscreen mode Exit fullscreen mode

Core and Feature Libraries Structure

Core and feature libraries in our workspace have similar structure. Library elements are grouped by their type: components, services, models, etc. Components inside of library can share the same services, models, and other elements.

    └── src/
        ├── lib/
        │   ├── directives/
        │   ├── components/
        │   │    └── cases-list/
        │   │        ├── assets/
        │   │        │   ├── config/
        │   │        │   │   └── config.json
        │   │        │   └── i18n/
        │   │        │       ├── es.json 
        │   │        │       └── en.json
        │   │        ├── cases-list.component.scss
        │   │        ├── cases-list.component.ts
        │   │        └── cases-list.component.html
        │   ├── models/
        │   │    └── case.model.ts
        │   │    
        │   ├── services/
        │   │    └── cases.service.ts
        │   ├── store/
        │   ├── tokens.ts
        │   └── routes.ts
        └── index.ts
Enter fullscreen mode Exit fullscreen mode
  • components - Angular standalone components specific to the feature.
    • assets - Configuration, i18n files, images, and other assets.
    • i18n - Internationalization JSON files.
    • config - Configuration JSON files for forms and tables configuration.
  • models - Data models, enums and interfaces used by the feature.
  • services - Feature-specific services.
  • store - State management using NgRx or other state management libraries.
  • tokens.ts - Contains Injection Tokens needed for library configuration.
  • routes.ts - Routes to show default feature functionality (only for feature libraries).
  • index.ts - Exports all components, services, and models from the library.

Pay attention that all assets are located close to the component that uses them. This approach makes it easier to manage them.

Create Library

Let's create "Cases" feature library as an example:

npx nx g @nx/angular:library libs/feature/cases  --publishable --unitTestRunner=jest --style=scss --importPath=@acme/feature-cases --tags=feature --prefix=acm
Enter fullscreen mode Exit fullscreen mode

This command creates a new library named cases in the libs/feature directory.
The library is created with the following options:

  • --publishable: Indicates that the library can be published as an NPM package.
  • --unitTestRunner=jest: Specifies that Jest will be used for unit testing.
  • --style=scss: Specifies that SCSS will be used for styling.
  • --importPath=@acme/feature-cases: Specifies the import path for the library.
  • --tags=feature: Tags the library as a feature library.
  • --prefix=acm: Specifies the prefix for the components in the library. In our case it is acm (Acme).

Pay attention to the tags option. It helps to enforce architectural rules, such as preventing cross dependencies (Enforce Module Boundaries on nx.dev).

Refer to the Nx Angular library documentation for more details on creating Angular libraries.

The nx generator has created component with the same name as the library, located in the root of the library
libs/feature/cases/src/lib/cases. This is not what we wanted, but there is no way to skip this component creation, let's remove it and create our own components:
Go to libs/feature/cases/src/lib and create a new component:

  npx nx g @nx/angular:component  libs/feature/cases/src/lib/components/cases/cases.ts --prefix=Acm
Enter fullscreen mode Exit fullscreen mode

This command creates a new component named cases in the libs/feature/cases/src/lib/components/cases directory.

The component is created with the following options:

  • --prefix=Acm: Specifies the prefix for the component. In our case it is Acm (Acme).

Refer to the Nx Angular component documentation for more details on creating Angular components.

Library Distribution

The standard method for distributing of libraries is through NPM packages. Depends on company policies, libraries can be published to public or private NPM registries. A private NPM registry can be established using tools like Nexus, Verdaccio, or by using the NPM.js registry. Choice of registry solution depends on company policies and requirements.

This approach allows seamless integration and version management across different client projects. Look at the command we used for cases library generation. It has --publishable option, which means that the library can be published as an NPM package. Nx automatically creates a package.json file in the library folder, which is used for publishing the library. All libraries of project should be publishable.

For versioning, building and publishing Nx provides a commands:

npx nx build @acme/feature-cases
npx nx publish @acme/feature-cases
Enter fullscreen mode Exit fullscreen mode

Refer to the Nx publish documentation and for more details on publishing libraries.

NPM distribution

Another aspect of library distribution is to ensure that all dependencies are installed in the application. Libraries can have dependencies on other project or external libraries.To ensure that all dependencies are installed in the application, we need to define them in the peerDependencies section of the library's package.json file.

For instance, AcmTasksListComponent, belonging to @acme/feature-tasks library, uses Table component from @angular/cdk library, also it uses controls and permissions functionality from acme libraries @acme/core-ui and @acme/core-permissions. To be sure that dependencies are installed in the application automatically, we need to define them in peerDependencies section of library package.json file:

{
  "peerDependencies": {
    "@angular/cdk": "^19.0.0",
    "@acme/core-ui": "^1.0.0",
    "@acme/core-permissions": "^1.0.0"      
  }
}
Enter fullscreen mode Exit fullscreen mode

To automate this process, we can use ESLint @nx/dependency-checks rule. Process is described in Nx ESLint dependency-checks-rule. You need update .eslintrc.json file and every time you execute linter command, ESLint will check if all dependencies are defined in peerDependencies section of library package.json file. Usually linter is executed as part of husky pre-commit hook, so you will be notified about missing dependencies before commit, or it can be fixed automatically using --fix option.

Versioning of libraries is another important aspect of library distribution.
Semantic Versioning (SemVer) is a widely adopted versioning scheme that conveys meaning about the underlying changes in a release.

Versioning strategy can be different, there are two common approaches:

  1. Independent Versioning: Each library has its own version number, which is incremented independently based on the changes made to that library.
  2. Lock-step Versioning: All libraries share the same version number, which is incremented whenever any library is updated.

Right now for Acme Case Management project lock-step versioning seems to be the best approach, because all libraries usually updated together.
This approach simplifies version management and ensures compatibility between libraries. But if in future micro frontend architecture will be implemented, and we can switch to independent versioning.
It brings some overhead in versions management and testing different scenarios, but it provides more flexibility in libraries updates.

Assets files

Almost every library of the project contains assets: i18n resources, form configuration files etc. Some libraries may contain
dozens of configuration files. It is important to keep assets close to standalone components and distribute them

along with the library.

Also, we need include these files into applications. There are a few ways to do it:

  1. Modify application's project.json file to include every asset from the library. Works well for small number of assets, but when library is growing and can contain hundreds of assets, this approach becomes unmanageable.
  2. Automate this process using Nx custom "copy" task, which will copy assets from the library to the application on building stage. This approach is more flexible and allows to manage assets in a more efficient way.

Web components

Web component creation is straightforward process, but distribution of web components can be is a bit tricky.

For our scenario (supporting legacy applications with outdated building tooling and security concerns that don't allow us to use CDN servers), the best approach is to distribute web components as NPM packages. These packages can be installed in the legacy application on building stage. This approach allows to use web components in legacy applications without any significant changes in the host legacy application. Web components should be distributed as prebuilt bundle ready to import to the legacy application.

Nx doesn't provide out-of-the-box support for web components distribution as npm-package. It requires some additional configuration.

Micro frontend

Micro frontend architecture is a modern approach to building web applications, where the application is divided into smaller, independent parts called micro frontends. Each micro frontend can be developed, deployed, and maintained independently. In the shell application, micro frontends can be loaded dynamically, allowing for a more flexible and modular architecture.
For our scenario we split the application into the next parts:

  1. Shell - The main application that loads micro frontends.
  2. Cases - Cases management micro frontend.
  3. Tasks - Tasks management micro frontend.
  4. Users - Users management micro frontend.
  5. Admin - Administration micro frontend.

Micro frontend runtime composition

Nx produces application for every micro frontend. But micro frontend application itself doesn't contain any business logic, it is a wrapper that loads features, defined in libraries.

Micro frontends are not always necessary. For single-tenant monolithic apps, Nx libraries provide enough modularity. But in SaaS or multi-tenant scenarios where clients must extend functionality at runtime, module federation allows dynamic composition.

Follow Nx Micro Frontend Architecture for more details on micro frontend applications based on Nx.

Conclusion

By adopting feature oriented design and Nx monorepo, Acme Corp can proceed with modernizing its legacy application step by step. Focusing on features, following clean architecture principles, helps company to keep libraries independent and maintainable.

Like Lego bricks, features can be combined into monolithic applications, distributed as NPM packages, web components, or assembled into micro frontends — providing the flexibility to adapt to any client or future architecture.

Top comments (0)