All Articles

Nx Workspace structure for an application with NestJS and Angular

Original Gist


Below is the sample folder structure for Nx with NestJS and Angular. Our principles are:

  • SCAMs (single component Angular modules) for tree-shakable components, meaning each component will have a respective module. For example, a RegisterComponent will have a corresponding RegisterModule, we won’t declare RegisterComponent as part of AuthModule for example.
  • Mostly everything will stay in the libs folder. New modules, new models, new configurations, new components etc… are in libs. libs should be separated into different directories based on existing apps. We won’t put them inside the apps folder. For example in an Angular, it contains the main.ts, app.component.ts and app.module.ts


└── root
    ├── apps
    │   ├── api (nestjs)
    │   └── client (angular)
    └── libs (1)
        ├── api (dir)
        │   ├── core (dir)
        │   │   └── feature (nest:lib) (2)
        │   ├── feature-1 (dir)
        │   │   ├── data-access (nest:lib, service + entities)
        │   │   ├── feature (nest:lib, module + controller)
        │   │   └── utils (nest:lib, things like interceptors, guards, pipes etc...)
        │   └── feature-2 (dir)
        │       ├── data-access (nest:lib, service + entities)
        │       ├── feature (nest:lib, module + controller)
        │       └── utils (nest:lib, things like interceptors, guards, pipes etc...)
        ├── client (dir)
        │   ├── shell (dir)
        │   │   └── feature (angular:lib) (3)
        │   └── feature-1 (dir)
        │       ├── data-access (angular:lib, service, API calls, state management)
        │       ├── feature (4)
        │       │   ├── list (angular:lib e.g. ProductList)
        │       │   └── detail (angular:lib e.g. ProductDetail)
        │       ├── ui (dir)
        │       │   ├── comp-1 (angular:lib, SCAM for Component)
        │       │   └── pipe-1 (angular:lib, SCAM for Pipe)
        │       └── shared (dir)
        │           ├── data-access (angular:lib, any Service or State management to share across the Client app)
        │           ├── ui (dir, 5)
        │           └── utils (angular:lib, usually shared Guards, Interceptors, Validators...)
        └── shared (dir, most libs in here are buildable @nrwl/angular:lib)
            ├── data-access (my shared data-access is usually models, so it is a lib)
            ├── ui (optional dir, if I have multiple client apps)
            └── utils (optional dir, usually validation logic or shared utilities)
                ├── util1 (lib)
                └── util2 (lib)

(1) lib vs dir

  • a dir is just a directory.
  • a lib is generated by using Nx schematics

(2) api-core-feature: this is the CoreModule that will include all initial setups like Config and Database Connection etc… and importing other Modules. CoreModule will be imported by AppModule

(3) client-shell-feature: Same idea as NestJS’s CoreModule. This Shell includes RouterModule.forRoot()

(4) client-feature-1-feature: This can either a dir or a lib.

  • If this feature only has one Routable component, it is a lib.
  • If it has multiple Routable components, then it should be a dir. For example:
└── feature
    ├── list (angular:lib e.g ProductList)
    └── detail (angular:lib e.g. ProductDetail)

feature usually contains the ContainerComponent and the RouterModule.forChild()

(5) client-shared-ui is a little tricky. The general recommendation is to NOT grouped stuffs by type like components, pipes etc… into a single module but because these are shared, it is easy to get quite messy if not grouped by type. This is your call. We prefer to have a Single Component Per Module (SCAM) for each angular library.

This structure is proposed by my friend Chau Tran and I am applying it for my latest project!


Following the above structure will bring three advantages:

  • Consistency: eliminate mental overhead when we don’t have to think about where to put what in a big repo having from two apps and above.
  • Promote Single Component Per Module (SCAM) + Buildable libraries to get the benefits from the nx affected commands.
  • Prevent circular dependencies issue.

Some rules of thumb

  • data-access: data-access can import other data-access. But never import its feature . For example: user/data-access can import from product/data-access but it will never import from user/feature
  • feature: can only import its own data-access or the global shared/data-access. For example: user/feature can import from user/data-access but never from product/data-access.
  • util: Utils can be shared across data-access, util.
Published 24 Mar 2021

Recent Posts

TypeScript unknown vs any types

The main difference between unknown and any is that unknown is much less permissive than any: we have to do some form of checking before performing most operations on values of type unknown, whereas we don't have to do any checks before performing operations on values of type any.

SVG fill color doesn't work with hex colors

Because # in URLs starts a fragment identifier. So, in order to make that work, write %23 instead of #, that is the value of escaped # character.

Follow @tuantrungvo on Twitter for more!