WTF is Nrwl nx?

What is Nrwl nx?

An opinionated set of tooling written by some clever people to help with writing enterprise level Angular applications.

It involves working within a monorepo, or ‘workspace’ as they refer to it, which has multiple apps in it, and extracting functionality out into reusable libraries to improve code reusability and decoupling.

You should seriously check it out:

https://nrwl.io/nx

Why should I care?

My team has a need to support development of a front end layer for a white label product, with multiple, localized, themed, customisable instances of the product to be sold to different clients.

A typical Angular feature architecture does not optimally support this requirement.

We need to minimise code duplication, yet retain the ability to support apps with different functionality and theming.

We took the decision to use a workflow whereby almost all code is extracted into reusable libs inside a monorepo.

Our apps then become nothing more than configuration: a place to assemble the libs and theming information needed to personalise and customise the application for a specific client.

This allows for massively faster development on a per-client basis, and any new functionality can be shared among all client apps, meaning we are continually improving our core product, while supporting multiple applications.

We are using Nrwl nx tooling to achieve this and so far it has been extremely nice to work with.

What is a monorepo and why use one?

There is some disagreement about the merits of monorepos, and many other people can explain the nuances better than me. It is worth Googling if you are new to the concept. Here are some starting points:

https://danluu.com/monorepo/ https://medium.com/@maoberlehner/monorepos-in-the-wild-33c6eb246cb9

However, on a basic level we took the decision to use a monorepo because:

  • It supports a CI/CD pipeline. All code has to go through the same set of automated tests before it is merged.
  • We can ensure code standards are clear and enforcable by putting all code through the same linting and formatting rules.
  • Code is easily shareable between applications.

What are libs?

Nrwl nx introduces the concept of libs. These are standalone, decoupled pieces of functionality for an Angular application. They can be thought of as the building blocks that we use to create our applications.

There are a number of different types of lib that we have defined, and put in separate directories, to make it easier for developers to know where to put a particular piece of code:

  • features – a standard Angular feature module. It may be composed of other features. It may have services, state management, routing, container/smart components and dumb/presentational components.

  • api – an Angular module with a service for calling a REST endpoint associated with a specific resource. For example a ‘cars’ api module would handle all http verbs associated with cars (‘Get’, ‘Put’ etc.). These are deliberately granular and are imported by feature modules for making api calls.

  • components – any shared dumb components that don’t belong in a feature module. These should not interact with a sandbox(see ‘what is a sandbox?’), and should be purely presentational.

  • utils – any heavy lifting code that isn’t already in a feature level service. This is pure business logic and should not deal with rendering.

  • core – functionality that every application needs. For example top level error handling.

  • scss – sass utilities. For example mixins, variables etc.

What are apps?

Apps are just Angular applications. However they are very slim. They shouldn’t have any more in them than the following:

  • Top level lazy loaded routing.
  • Custom theming information (fonts, color palette, icons etc.).
  • Assets.
  • App specific environment variables.
  • E2E tests.

Everything else should be in a standalone lib somewhere.

Feature level architecture

Of the libs folders, the features one is probably the most complex.

As an example, a user-registration feature might look like below (NB spec files have been excluded for clarity but all files with logic in them will have an equivalent spec file):

/libs
  /features
    /user-registration
      /src
        + +state
          - ...
        + containers
          - ...
        + components
          - ...
        + models
          - ...
        + services
          - ...
        - user-registration.module.ts
        - user-registration.sandbox.ts
      - index.ts

Here are the interesting bits in order:

  • +state folder is filled with ngrx redux stuff (actions, reducers, effects).

  • containers folder is filled with ‘container’ or ‘smart’ components. These are components that interact with the sandbox (don’t worry, sandbox will be explained below… ).

  • components folder is filled with ‘dumb’ or ‘presentational’ components. These simply have data flowed into and out of them. They don’t know about anything else, hence they are dumb.

  • models folder is filled with typescript interfaces describing any data types associated with the feature module.

  • services folder is filled with services. Any pure business logic associated with the feature should live in a service here.

  • user-registration.sandbox.ts is a sandbox.

What is a sandbox?

Good question.

In the most basic terms, it is an abstraction layer between smart container components and state.

It allows our smart container components to deal with a clean and descriptive interface, rather than talking directly to redux.

It exposes methods for updating state, and streams of data for flowing state out of the sandbox.

The thinking is that smart components don’t really care that their data is coming from an ngrx store, so perhaps they shouldn’t know.

The reason it is called a sandbox is that we stole the name and the concept from here:

https://blog.strongbrew.io/A-scalable-angular2-architecture/ https://blog.strongbrew.io/A-scalable-angular-architecture-part2/

We have tweaked the original concept slightly, in that we still make any http calls via effects, but other than that it is pretty similar.

The name ‘sandbox’ apparently is used because in a sandbox, you can only play with the toys you are allowed access to.

Here is an example to hopefully make the concept clearer:

user-registration.sandbox.ts:

import { Injectable } from '@angular/core';
import { select, Store } from '@ngrx/store';
import { Observable } from 'rxjs/Observable';

import { SecurityQuestion } from '@ten-platform-app/api/api-security-questions';

import { UserRegistrationStateMap, getSecurityQuestions } from './+state';
import { LoadSecurityQuestionsAction } from './+state/security-questions.actions';

@Injectable()
export class UserRegistrationSandbox {
  // A publicly exposed stream of security questions that can be hooked into by a smart component.
  public securityQuestions: Observable<SecurityQuestion[]> = this.store.pipe(
    select(getSecurityQuestions),
  );

  // The sandbox has our feature level redux store passed into it.
  constructor(private store: Store<UserRegistrationState>) {}

  // Our smart component uses this method to request security questions, and subscribes to the public stream above.
  public getSecurityQuestions() {
    this.store.dispatch(new LoadSecurityQuestionsAction());
  }
}

How do we structure our state management code?

We use the redux pattern to handle state in our libs:

https://redux.js.org/introduction.

We use the ngrx implementation of redux:

https://github.com/ngrx

This is what the +state folder looks like for a typical module:

  /+state
    - user.actions.ts
    - user.actions.spec.ts
    - user.effects.ts
    - user.effects.spec.ts
    - user.reducer.ts
    - user.reducer.spec.ts

There are three types of file, actions, effects and reducer.

In our project these files are generated using custom angular schematics.

These schematics work the same way the angular cli does, and will generate a basic store that works. This cuts down on boilerplate code and helps us to standardise how we write our state management code.

Actions and reducers are very much like any other redux implementation, with the slight difference that everything is strongly typed, using Typescript.

Effects

Effects are used to handle asynchronous code. For example, any http call will be done inside an effect.

Our effects have some specific bits of code in them that allow them to work with our sandbox layer and should be explained:

import { Injectable } from '@angular/core';
import { Effect, Actions } from '@ngrx/effects';
import { DataPersistence } from '@nrwl/nx';
import { of } from 'rxjs/observable/of';
import { map } from 'rxjs/operators';

import { ApiSecurityQuestionsService } from '@ten-platform-app/api/api-security-questions';

import { UserRegistrationFeatureStore } from './index';
import {
  LoadSecurityQuestionsAction,
  LoadSecurityQuestionsFailureAction,
  LoadSecurityQuestionsSuccessAction,
  SecurityQuestionsActionTypes,
} from './security-questions.actions';

@Injectable()
export class SecurityQuestionsEffects {
  @Effect()
  public loadSecurityQuestions = this.dataPersistence.fetch<
    LoadSecurityQuestionsAction
  >(SecurityQuestionsActionTypes.LOAD_SECURITY_QUESTIONS, {
    run: (
      action: LoadSecurityQuestionsAction,
      state: UserRegistrationFeatureStore,
    ) => {
      // check state for existing security questions.
      // Only make call to get them from the apiService if needed.
      if (state.userRegistration.securityQuestions.loaded) {
        return of(
          new LoadSecurityQuestionsSuccessAction({
            questions: state.userRegistration.securityQuestions.data,
          }),
        );
      } else {
        // Delegate the api call to the relevant service.
        return this.apiService.getSecurityQuestions().pipe(
          map(questions => {
            return new LoadSecurityQuestionsSuccessAction({
              questions: questions,
            });
          }),
        );
      }
    },

    onError: (action: LoadSecurityQuestionsAction, error: any) => {
      return new LoadSecurityQuestionsFailureAction({ error });
    },
  });

  constructor(
    private actions: Actions,
    // This is a utility provided by Nrwl/nx.
    // It has convenience methods that eliminate race conditions
    // and ensure consistency when fetching data
    private dataPersistence: DataPersistence<UserRegistrationFeatureStore>,
    // A granular api service is imported for actually making REST calls.
    // This follows the principle of single responsibility.
    private apiService: ApiSecurityQuestionsService,
  ) {}
}

Note that the effects class does not directly interact with the httpClient. This is delegated to a granular api service, which handles the api call and any data transformation before returning a stream of appropriately typed data.

Effects work by watching a global stream of actions.

If an action of the right type is seen (in the example above a LOAD_SECURITY_QUESTIONS action), then some piece of asynchronous code is run.

Once that code has finished, a new stream is returned, with a new action.

So basically effects just map action streams to other action streams, and do a bit of work in the middle.

Redux alone is not able to smoothly handle asynchronous code, so these side effects are handled inside effects.

In addition, effects are where we handle optimistic and pessimistic updates and caching of data.

Again, Nrwl nx provides convenience methods for optimistic and pessimistic updates:

https://nrwl.io/nx/guide-data-persistence

Conclusion

Architecting Angular applications in a way that allows multiple developers to work efficiently together with minimal friction is a tough job.

This is my team’s attempt to solve some of those problems. Hopefully it may help other teams too!

Leave a Reply

Your email address will not be published. Required fields are marked *