Skip to content

Instantly share code, notes, and snippets.

@trungvose
Last active June 9, 2024 12:26
Show Gist options
  • Save trungvose/7ef8766cafc05bc8fd87be22de6c5b12 to your computer and use it in GitHub Desktop.
Save trungvose/7ef8766cafc05bc8fd87be22de6c5b12 to your computer and use it in GitHub Desktop.
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                       <-- grouping folder (dir)
        │   ├── core                  <-- grouping folder (dir)
        │   │   └── feature           <-- nest:lib (2)
        │   ├── feature-1             <-- grouping folder (dir)
        │   │   ├── data-access       <-- nest:lib, service + entities
        │   │   ├── feature           <-- nest:lib, module + controller
        │   │   └── utils             <-- nest:lib, things like interceptors, guards, pipes etc...
        │   └── feature-2             <-- grouping folder (dir)
        │       ├── data-access       <-- nest:lib, service + entities
        │       ├── feature           <-- nest:lib, module + controller
        │       └── utils             <-- nest:lib, things like interceptors, guards, pipes etc...
        ├── client                    <-- grouping folder (dir)
        │   ├── shell                 <-- grouping folder (dir) 
        │   │   └── feature           <-- angular:lib (3)
        │   ├── feature-1             <-- grouping folder (dir)
        │   │   ├── data-access       <-- angular:lib, service, API calls, state management)
        │   │   ├── feature           <-- grouping folder (dir) or lib (4)
        │   │   │   ├── list          <-- angular:lib e.g. ProductList
        │   │   │   └── detail        <-- angular:lib e.g. ProductDetail
        │   │   └── ui                <-- grouping folder (dir)
        │   │       ├── comp-1        <-- angular:lib, SCAM for Component
        │   │       └── pipe-1        <-- angular:lib, SCAM for Pipe
        │   └── shared                <-- grouping folder (dir)
        │       ├── data-access       <-- angular:lib, any Service or State management to share across the Client app)
        │       ├── ui                <-- grouping folder (dir) (5)
        │       └── utils             <-- angular:lib, usually shared Guards, Interceptors, Validators...)
        └── shared                    <-- grouping folder (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 grouping folder (dir), if I have multiple client apps
            └── utils                 <-- optional grouping folder (dir), usually validation logic or shared utilities
                ├── util1             <-- lib
                └── util2             <-- lib
  1. lib vs grouping folder (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.

Example

https://github.com/trungk18/angular-spotify

@pschild
Copy link

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
Copy link

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
Copy link

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
Copy link

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

@trungvose
Copy link
Author

@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
Copy link

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
Copy link

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
Copy link

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
Copy link

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
Copy link

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.

@Benny739
Copy link

Benny739 commented Aug 6, 2021

Hi @trungk18,

I see you are putting guards in utils, how do you deal with needing to access data in the guards?
Where do you usually put shared guards or directives/components which need to access data but are also shared widely through the app? Is a shared feature the right thing here? E.g. a guard which checks if a user has a specific role or a directive which unlocks features if the user has unlocked them.
Also in the backend a guard which loads the user from the db and checks his roles.
Util feels like the right thing but utils can not import data-access.

@nartc
Copy link

nartc commented Aug 9, 2021

@Benny739 Hi, thank you for the input. This is actually a valid argument. I did look into this again over the weekend and realized that it was my mistake that I thought utils is able to depend on data-access. With that said, I'm thinking about where I should put the guards and interceptors and the likes, basically providedIn: root services that aren't really services. What is your suggestion>

@tonivj5
Copy link

tonivj5 commented Aug 9, 2021

I'm interested on this too 👀

@Benny739
Copy link

Benny739 commented Aug 9, 2021

Hi @nartc and @tonivj5,

we added a 5th type in our app called "service". This type can import everything besides type:feature. We had multiple problems without this 5th type:

  • Smart guards
  • Global services or services that where shared between features
  • Smart components that are shared between features

With this new type we're having no problems now in our app and everything works how it should.

We also added 2 more layers besides type: platform & scope. Basically to have more order in what can import what (backend, frontend, android etc) and also to make everything more domain specific. E.g. we had multiple services which where shared between multiple frontend apps but belonged to 1 domain. In this case:
platform: frontend -> can be imported by any frontend project
scope: domain1 -> can only be imported by domain1
type: service -> as described above

@nartc
Copy link

nartc commented Aug 9, 2021

@Benny739 Thanks for sharing! That is a valid approach. In one of our projects, we actually add a directory called “infra” to group data-access and other stuffs like guards and interceptors. In some other projects, we’d have different types like yours. But most of the times, I made a mistake of having utils importing data-access like the proposed structure here.

Again, thanks for your input!

@Benny739
Copy link

Benny739 commented Aug 9, 2021

@nartc I just had a look in the nx book again (part2, starting page 13) and see now why things like guards can be in type util. Nx recommends to put state management in utils. So data-access imports the store but doesn't own it. I usually put my state management in data-access and guards felt wrong here.
If we put state management in utils we can also put the global services and guards here.

If anybody is looking for the nx book: https://go.nrwl.io/angular-enterprise-monorepo-patterns-new-book

Edit: On page 14 they say state management belongs to utils, but on page 17 they say state management belongs to data-access. Pretty confusing

@Falven
Copy link

Falven commented Feb 5, 2022

@Benny739

Hi @nartc and @tonivj5,
we added a 5th type in our app called "service". This type can import everything besides type:feature. We had multiple problems without this 5th type:

  • Smart guards

Not sure what you mean by "smart guards" or why they can't reside in libs/api/feature/utils or libs/api/shared/utils/.

  • Global services or services that where shared between features

Why couldn't you include services shared between features in libs/api/shared/data-access?

  • Smart components that are shared between features

Why not libs/client/shared/ui?

With this new type we're having no problems now in our app and everything works how it should.

We also added 2 more layers besides type: platform & scope. Basically to have more order in what can import what (backend, frontend, android etc) and also to make everything more domain specific. E.g. we had multiple services which where shared between multiple frontend apps but belonged to 1 domain. In this case:

Why not keep these services under their respective lib domain directory under data-access and just reference that from your other domains?

platform: frontend -> can be imported by any frontend project
scope: domain1 -> can only be imported by domain1
type: service -> as described above

@Falven
Copy link

Falven commented Feb 5, 2022

Hi @trungk18, @nartc,
I have a couple of disagreements with your choice structure:

  1. I don't think you should have top-level api and client directories.
  • You should keep implementation details or file types out of your structure and constrain it to domains and features as specified in the Angular Enterprise MonoRepo Patterns book page 14.
  • This will also flatten your structure greatly and make your MonoRepo more navigable, manageable and understandable, by reducing your project name lengths.
  • Instead, you should be using platform: or framework: tags (usually, the more specific you can be, the better, within reason), as specified in the Angular Enterprise MonoRepo Patterns book page 14 along with lint rules to impose project constraints.
  1. This is more of a nitpick, but I also realize that you chose data-access as it's specified in the book.
  • However, I prefer to name such projects domain as they don't just include data-access (services, repositories...), but also models (dto's, interfaces...) as well as business logic.
  • Depending on how large your domain projects are, you could also convert domain into a directory, and split the implementation down further into data-access, models, and business projects to take further advantage of NX dependency graph optimizations.

@nartc
Copy link

nartc commented Feb 5, 2022

@Falven Thank you for the comment and for voicing your suggestions.

I think, as most questions in programming, that it depends on the teams and projects. I personally like grouping top directories because I prefer have clear “path” in the import: @scope/app-dir/lib instead of @scope/app-lib

Also, I rarely change the module names that get generated and I pefer to have shorter module names than longer.

Last but not least, the lib type is very dependent on projects. I like domains a lot and have used it before. Lately, I use infra as a lib type as well.

@Frankitch
Copy link

@nartc @trungk18 Thank you for sharing your work, it's awesome!
I have a couple of questions for you if you don't mind :)

I'd like to use SCAM as well but I'm reluctant to create one lib per component. I understand that you follow this principle to leverage partial builds but are there other benefits? IMHO, it seems a big cognitive overhead to have so many libs because creating a new lib generates a bunch of new configuration files and that makes browsing files harder in the long run. By the way, are dependencies graph still readable in this case?

In my company, I have two more "dimensions" : I have two business apps each with a back and a front office. How would you handle this case?
Would you keep the structure as flat as possible or would you nest folders like this:

.
└── apps
    ├── business-app1
    │   ├── front-office
    │   │   ├── client
    │   │   └── api
    │   └── back-office
    │       ├── client
    │       └── api
    └── business-app2
        ├── front-office
        │   ├── client
        │   └── api
        └── back-office
            ├── client
            └── api

And how would you organize libs to share libs between the client and the api of a same business app and between api (or client) of different business apps?

I have the feeling that it can quickly get complex whereas we are a small company and a small team dev!

Keep up the good work!

@pcurrivan
Copy link

@nartc Is disabling "showCircularDependencies" still an option in the current version of Nx? Seems like the config files have changed since you recommended disabling this option for TypeORM entity libs. Is there somewhere else to put this option or an equivalent option, or is that strategy no longer viable? It seems like the other option (putting all entities in one shared lib) would be a chokepoint in the dep graph, causing everything using any entity to be considered affected when any entity changes.

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