Skip to content

Instantly share code, notes, and snippets.

Embed
What would you like to do?
Nx workspace structure for NestJS and Angular

Nx

https://nx.dev/

Nx is a suite of powerful, extensible dev tools to help you architect, test, and build at any scale — integrating seamlessly with modern technologies and libraries while providing a robust CLI, caching, dependency management, and more.

It has first-class support for many frontend and backend technologies, so its documentation comes in multiple flavours.

Principles

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

Structure

.
└── 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
  1. 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
  2. client-shell-feature: Same idea as NestJS's CoreModule. This Shell includes RouterModule.forRoot()
  3. 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()

  1. 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!

Why?

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-accessdata-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-accessutil.
@pschild

This comment has been minimized.

Copy link

@pschild pschild commented Mar 26, 2021

Hi @trungk18,

thanks for sharing this, looks like a very useful concept to me! 👍
One question comes to my mind though regarding the advantage Prevent circular dependencies:

Imagine I have two libraries lib/api/user and lib/api/photo. When I now want to configure database relations between those libs using an ORM lib like TypeORM, I need something like this (example based on https://github.com/typeorm/typeorm/blob/master/docs/many-to-one-one-to-many-relations.md):

// libs/api/photo/data-access/entities/photo.entity.ts:

import {User} from "@example/user/data-access/entities/user.entity.ts";

@Entity()
export class Photo {
   @ManyToOne(() => User, user => user.photos)
   user: User;
}


// libs/api/user/data-access/entities/user.entity.ts:

import {Photo} from "@example/photo/data-access/entities/photo.entity.ts";

@Entity()
export class User {
   @OneToMany(() => Photo, photo => photo.user)
   photos: Photo[];
}

That would result in circular dependencies between the two libs. Do you have any suggestions how to solve this? 😉

@nartc

This comment has been minimized.

Copy link

@nartc nartc commented Mar 26, 2021

@pschild Hi, I'll be giving you my take on this, hopefully it answers your question.

This is a dreaded issue for NestJS developers using Nx. There's no real way to prevent this issue unfortunately because it is required to establish the relationship. The lazy-evaluate () => syntax helps with the actual circular dependency.

There are two approaches that you can take:

  • For NestJS, you can have something like the following:
.
└── libs
    └── api
        ├── feature-1
        │   └── data-access (instead of lib, make it a dir)
        │       ├── entity (lib, this houses the entities for feature-1)
        │       │   └── feature-1.entity.ts
        │       └── services (lib, other data-access related stuffs, you can call it whatever makes sense)
        └── feature-2
            └── data-access (instead of lib, make it a dir)
                ├── entity (same as above)
                │   └── feature-2.entity.ts
                └── services (same as above)

then in angular.json (or workspace.json), locate these entity libs and add showCircularDependencies: false to the build.options

  • Another approach that I've seen is entities becomes a lib in shared/data-access/entities
.
└── libs
    └── api
        ├── feature-1
        │   └── data-access (lib, but without entities)
        ├── feature-2
        │   └── data-access (lib, but without entities)
        └── shared
            └── data-access (dir)
                └── entities (lib, house entities for the entire app)
                    ├── feature-1.entity.ts
                    └── feature-2.entity.ts

Again, I hope that answers your question or at least gives you some idea. Thanks for the kind words!

@royling

This comment has been minimized.

Copy link

@royling royling commented Mar 31, 2021

@trungk18 @nartc really great and helpful notes.
I see the mentioned principles are applied in https://github.com/trungk18/angular-spotify project that makes the project simple and easy to understand, especially SCAM, great work!
One thing that I'd like to hear your thoughts about is:
ng-packagr is used for building angular libs under the hood, which supports secondary entry points. That implies you may not have to create so many libs (large angular.json) at all, to achieve SCAM.
Do you think if that may be better way to manage these SCAM libs?

@nartc

This comment has been minimized.

Copy link

@nartc nartc commented Mar 31, 2021

@royling Thanks for the kind words. It means a lot.

As far as ng-packagr goes, I still think it’s beneficial to create lib using Nx. Everything is just setup for you. With everything in angular.json, you can orchestra testing, linting, and building however you like by manipulating the architect portion. Not to mention the computation caching and module boundary you get from Nx lib

That said, I understand the concern about angular.json size and I’d suggest not to worry about that. My angular json is around 3-4k lines and I rarely touch it, I can use the CLI to modify it from the terminal then check git to ensure it’s correct, without having to open it at all.

Hope this answers your question

@trungk18

This comment has been minimized.

Copy link
Owner Author

@trungk18 trungk18 commented Mar 31, 2021

@royling Thanks for dropping by, and I am glad that you like our code :)

If I understand correctly, what you mentioned was, for example.

  • You have a button and a checkbox.
  • If we follow SCAM, you will have ButtonComponent, ButtonModule and CheckboxComponent, CheckboxModule
  • Currently, we create one Angular lib for storing ButtonComponent, ButtonModule and one Angular lib for storing CheckboxComponent, CheckboxModule

What you are asking is instead of creating two Angular libraries for storing two SCAM. Should we put them into a single Angular lib and utilize secondary entry points, and put them all together inside a standard lib, let call it ui-lib. My answer is we should not do that, because

  1. Following secondary entry points, you will have to follow ng-packagr structure. E.g. manually create button folder, with src folder inside and a package.json like
{
  "ngPackage": {}
}

Manually is not great, just create a new lib, and everything is automatically created for you.

  1. If you put all your code inside a single lib and make changes for a single component, the whole lib needs to be rebuilt instead of building only the lib that contains your changes.

What do you think?

@hrvbernardic

This comment has been minimized.

Copy link

@hrvbernardic hrvbernardic commented Mar 31, 2021

Nice job ! :). Just one quick question, where would you keep container components that are not routes by themselves, or even, container components that you would like to reuse across features. In my projects i usually end up with that scenario just so i don't duplicate much code for certain stuff. I'd really like to hear your opinion. Cheers!

@nartc

This comment has been minimized.

Copy link

@nartc nartc commented Mar 31, 2021

@hrvbernardic Hi, do you have an example of what you're talking about? I kind of have an idea but want to make sure we're on the same page first.

@hrvbernardic

This comment has been minimized.

Copy link

@hrvbernardic hrvbernardic commented Mar 31, 2021

@nartc Well, something like this. Let's say i have a page with pretty complex left and right sidebars with some list in the middle. Sidebars are shown only on this page and are used to control what is shown in the list or to update some items with various actions.

1.Not routable container example

  • in this case the page component would be responsible for layout mostly while sidebars and list component would be made containers
  • this is because page component as a container would grow substantially so I'd like to separate things a bit and communicate via store
    of some kind

2.Reusable container example

  • let's say the list container component I mentioned should be used in multiple places in an app but has some logic for fetching and updating remote data that doesn't change so I'd like to be able to use it without always having to wire up all outputs and input's from the dumb component part of that list

Maybe not the best example but i believe you'll get it. :)

@nartc

This comment has been minimized.

Copy link

@nartc nartc commented Apr 1, 2021

@hrvbernardic thanks for the example. For non-routable components, I usually put them in ui directory. In your example

  • Sidebars container components can stay in: feature-1/ui/sidebar-1 feature-1/ui/sidebar-2. They are SCAMs, can have their own Service (or ComponentStore if you use it).
  • List container component can be brought to: shared/ui/generic-list (or some name that notifies that this List is a generic list). And again, it'll be a SCAM

One problem that might be of concern is that there is no differentiation between Container Component vs Presentational Component. In this case, we can make use of the Tagging system of Nx to have some more contextual information. Other than that, I'd say discuss with teams (or yourself) to come up with a convention that makes sense. For example, break up ui into ui (for container component) and view (for presentational component).

Another option is nesting components and containers in ui like ui/containers and ui/components. Although the general consensus is against grouping things into type (containers, components are types) but I'd suggest that whatever makes sense to your team (and you), do it and do it consistently!

@royling

This comment has been minimized.

Copy link

@royling royling commented Apr 6, 2021

@nartc @trungk18 thanks for sharing your thoughts, that all make sense. And totally agree on the automation we can get from Nx.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment