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

@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