#NestJS Architecture & Advanced Patterns

26 messages ยท Page 1 of 1 (latest)

upper crescent
#

Hi! I recently completed the "NestJS Architecture & Advanced Patterns" course (which was excellent!), but I still feel a bit uncertain about communication between different modules.

The structure presented in this course looks like in the attachment.

The course primarily focused on a single "main" module - Alarms, which was well-organized and straightforward. However, I find it to be somewhat unrealistic.

Consider a scenario where we have a Products module instead. Following a similar structure, we would have a products/domain/product.ts model. However, all products typically belong to certain categories. In such a case, we'd include category: Category in our Product model, which ideally should reside in a separate Categories module, alongside its corresponding domain model, categories/domain/category.ts.

My question is: Is it appropriate for a product model to depend on a model from a different module? While they technically exist within the same layer, they are part of different modules. One alternative could be to redefine our module structure. Instead of separate Products and Categories modules, we could consolidate them into a single PIM (Product Information Management) module. However, this might lead to similar considerations for modules like Importer or Invoices down the line. Ultimately, some level of interconnection between models seems inevitable.

How are you, guys, solving it?

barren lark
#

Interesting they (yes, I'm not directly a part of the Nest core team) are teaching this file formatting. I personally don't like having to think in two dimensions like that. You have to basically know what part of nest is a port, a domain, or part of infra, etc. etc. then know how that component interacts inside the application. How does the CLI even work with this setup? ๐Ÿค” Does it? And this also kills reusability, I think. Can you share what the alarms module code looks like please? (I've never taken the course, so I'm interested.)

I wrote an article about this same topic.

If you still have questions about how to split up modules after reading that article, let me know.

DEV Community

So, you're just getting going with Nest, ey? You might not yet completely understand why you want to...

upper crescent
#

@barren lark thanks, I'll take a look and I'll let you know, for sure.

You can check the whole code in this repository; somebody has already linked it on Discord, so I'll do it too: https://github.com/micalevisk/nestjs-course-architecture-and-advanced-patterns/tree/main/src

In general, I like the concept of feature-modules, but I think in this scenario showed by the official course there's no way to have plug-and-play modules. You could copy the whole Alarms module to a different project because it's very simplistic, but in a real-case scenario, there's no way to separate models. Unless I'm missing something.
Of course, there has to be communication between modules, but that's mainly done by abstracting services and I don't see a way to do the same with models. That would be just wrong.

GitHub

Source code of the project developed in the NestJS course 'Architecture & Advanced Patterns' (https://courses.nestjs.com/#architecture) - micalevisk/nestjs-course-architectu...

barren lark
#

Hmm... in my opinion, modules shouldn't be sharing models. Can you explain your concern there with what you mean by "keep them seperate"?

upper crescent
#

I agree, in my opinion, modules shouldn't share models, but in this case: using the structure presented earlier and my example with products and categories, how would you do it?

There is a module Products (instead of Alarms), having a Products model inside (products/domain/product.ts ). The product model should have a "category" attribute, which is the type of the class Category.

class Product {
    id: number,
    category: Category,
}

In this case, where should the "Category" model be located? In which module?

#

or, maybe the model should have only categoryId

barren lark
#

It depends on what you want to do with categories. If they need to be product-specific only, then it should be modelled in the module that works on products. If it is more a generic or app-centric feature, it might need to be its own module.

#

Or, you might even end up with both. ๐Ÿ™‚

upper crescent
#

Yeah, I mean product-specific categories. So maybe something like PIM module having "submodules" Products and Categories? But then again, there will be a file import from Categories submodule inside of Products submodule, to import Category model.

And on top of that, let's say we have some ProductsImporter module that connects with some external API. Then somewhere we'll need to have a mapper that maps APIProduct model to our Product model. To do that, we have to import products/domain/product.ts file somewhere inside ProductsImporter anyway.

That's my concern. The module will never be plug-and-play in this scenario ๐Ÿค”

#

And if not model, then at least an interface will be, but I'm not sure if models should be wrapped in an interface.

barren lark
#

But then again, there will be a file import from Categories submodule inside of Products submodule, to import Category model.
This shouldn't be necessary. Your category service should abstract any model knowledge away from Products.

upper crescent
#

So, here:

class Product {
    id: number,
    category: Category,
}

Category model is not the same model as one living inside Categories module? Just an interface?

barren lark
#

You could create either an interface or just make a local definition (semi-copy) of the model needed. If they are totally intertwined, then you might be better off with just a product-categorization service, so the two can share the model.

I have to sort of take back what I said too. You could also just import the category class as a type too, despite the category feature being in its own module (if you design it that way). It's just that it then becomes a hard dependency.

In the end, would the Category type in Product really have everything in the Category model in the category module? I'd think not. You'd have a specialized Category type more than likely.

I see this all the time. Trying to save some key stokes or thinking breaking changes to a type might break the app in too many places, etc. blah, blah all in the name of thinking DRY is going to save the dev from extraneous work. That is wishful thinking. Encapsulation is the name of the game with modules and in many cases, it means duplication of code, where needed. I see this even worse happening inside the module, when people try to mix models/ entities with DTO classes. It is not a good practice.

The service and type (your model) can be taken as the "blue-print" for what is possible to get in a consuming app. What you actually use is what you define as the model/ type needed. Yes, if you separate them, you also know that if you change the type/ model of your service, you might be breaking consumers of that service. So, it is up to you to "communicate" that change to other devs (or to yourself) to make the necessary changes in consuming modules.

If you follow this methodology, your modules are standalone and can be plug-n-play. There are also other concerns you might run into, but I hope you get the rough idea. ๐Ÿ™‚

upper crescent
#

Well, donยดt get me wrong, Iยดm not trying to be just lazy ๐Ÿ˜… I try different approaches, looking for something that makes sense. Which is clean and reusable. I think I know concepts of clean code, hex architecture, DDD and so on but there always needs to be also some balance. Too many layers can become too messy at some point too. Or maybe wrong designed layers.

#

Maybe my example with models wasn't the best, let me explain with another one. Let's go back to our Alarms.
They suggested to have infrastructure catalogue inside our feature-module. So TypeORM's entity lives in src/alarms/infrastructure/persistence/orm/entities/alarm.entity.ts
And what if we have to add a relation to the UserEntity there? It would look like this (attachment)

#

Using TypeORM you need to user related entity class and import it. And in this scenario, you need to physically import UserEntity by import { UserEntity } from "../../../../../users/infrastructure/persistance/orm/user.entity";
I don't like the idea that the entity from Users module have to be imported in Alarms module. And, personally, I don't see a way to avoid it

#

I've read your article, and you've suggested having common/database module - and as I understand - having all TypeORM entities living there together. And that makes much more sense to me

#

I would simply like to know what idea the course authors had for solving problems like this one. Of course, nothing prevents you from importing this entity and everything will work perfectly fine, but then the module is, as you said, "strictly coupled" with another.
On the one hand, everything is fine, because we remain on the same infrastructure layer and communication is allowed here, but on the other hand, I won't be able to simply copy the alarm module into another project without prior modifications.
If the authors read what I write here - I encourage you to speak up, and maybe even record one small additional episode with one more module that works with the alarm module ๐Ÿ™‚

barren lark
#

and as I understand - having all TypeORM entities living there together

That would be incorrect. The database module I mentioned would only be for the rigging up of the module to access the database - the "forRoot" part of setting up the database. After that, each module would use the "forFeature" (thus the name) to build out the TypeORM services.

Your example of the user is what I meant about would you really be using the whole user object to fill in that piece of data? You'd probably be only filling in the name. So, the question is, can you store the name only? The fact you have that long ugly path should be a code smell. ๐Ÿ™‚ The AlarmEntity is also questionable. Do you need a whole entity in the array there? I don't use TypeORM at all, so I'm not keen to how to solve this best. But for sure, the level of abstract naming and file hierarchy definitely also adds to the problem. With what you put together with the user, you'll also run into circular dependency issues.

upper crescent
#

You'd probably be only filling in the name
Yes, but that's how I understand domain models and I fully agree with that.

In my last example though, we were in the infrastructure layer, with a ORM model. In this case, TypeORM entity also defines your DB structure, an entity represents your table. So here we'd have more attributes - as many, as many columns we have in the table. We can argue if TypeORM is a good tool or not, but it works this way. And it's not like I'm trying to connect two completely incompatible things - TypeORM is recommended by the official NestJS documentation and by NestJS team in their official course in the exact same file structure.

This is the reason why I am here and started this thread. To understand the idea behind the structure presented by the official team, because I'm not sure if I still don't understand something, or simply some principles of "clean code" have been slightly stretched ๐Ÿ™‚

barren lark
#

Well, as you can tell from my article, I prescribe a totally different structure. Much more simplistic.

Yep, inside a module you can bundle/ structure components practically as you'd like and with the Nest training example, they bundle it according to CA concepts. Maybe when you implement CQRS, such a structure makes more sense. Idk. ๐Ÿคท๐Ÿปโ€โ™‚๏ธ . I don't use CQRS.

Interestingly too, in theory, if you include a user module as in your example above, for the path to get the plug-n-play you are looking for, you should be using the CQRS bus system and not dependency injection. And with that line of communication, you have zero chance to "share" types between modules (or rather the model types). ๐Ÿค” You'd have DTOs or commands and/or query objects. Interesting too, they use both Mongoose and TypeORM in the tutorial example. Mongo is the query database. The RDBMS is the state database and CQRS is being used to keep them in sync.

upper crescent
#

So maybe that was the main idea behind it. They are using CQRS. I don't.
I liked your structure, I think it's better in most cases. It's simplistic ๐Ÿ˜‰

Btw, do you know some open source nest-based repos you think are well organized?

barren lark
#

Jay's work is pretty good stuff IMHO. https://github.com/jmcdo29

mossy hedge
# barren lark More specifically. https://github.com/jmcdo29/zeldaPlay

May I suggest https://github.com/jmcdo29/unteris instead? My zeldaplay application hasn't been touched in years, and there's definitely things I would do differently. The way I wrote raw queries and the database connection works, but it's a little jank and I would grab a query builder instead probably. Also, breaking out the modules into nx libraries instead of keeping them all in apps/api

GitHub

Unteris is a TTRP homebrew setting filled with cults, ancient magic, and deities. This is the source code for the website and server - jmcdo29/unteris

upper crescent
#

Well, anyways, thank you both @barren lark and @mossy hedge. I think this conversation has been very informative - the topic itself is complicated and the information covered in this thread may be useful to others as well ๐Ÿ™‚