Skip to content

Developer Insights

Join millions of viewers! Our engineers craft human-written articles solving real-world problems weekly. Enjoy fresh technical content and numerous interviews featuring modern web advancements with industry leaders and open-source authors.

Newest First
Tags:Monorepo
Decomposing a project using Nx - Part 2 cover image

Decomposing a project using Nx - Part 2

Decomposing a project using Nx - Part 2 Large projects come with a set of challenges that we need to remember in order to keep our codebases clean and maintainable. In the previous article, we talked about the horizontal decomposition strategy, and how it can help us manage our application code better. In this article, I would like to focus on the second strategy for splitting the application code - vertical decomposition. Vertical decomposition The more the application grows, the more it becomes important to create, and keep boundaries between certain sections of the application codebase. This is where the concept of vertical decomposition comes in. In most large-scale applications, we should be able to distinguish certain areas that concern different parts of the business value or different parts of user interaction. Let's use the slightly expanded version of the application used in the previous article. In addition to the liking and disliking functionality for photos, we can now see and edit the user's profile. You can find the relevant code on my GitHub repository. As in most cases, the interaction with the user profile here can be considered as a completely separate part of the application. This gives us the clue that this part of the codebase can also be separate. The distinction between modules that concern different scopes of the application is what I call a vertical decomposition. This creates a second axis on which we can split the codebase to minimize the concern that each part of the application needs to be aware of. We can imagine that, if the example application were to grow, we could create separate modules for them. E.g: - photos - photos related features - user - user profile feature - chat - chatting between users feature In the aforementioned example, we can see 3 possible parts of the application that don't have very strong dependencies between each other. Separating them upfront will ensure that we don't end up with too many entangled features. This requires more conceptual work in the beginning, but definitely pays off as the application grows, becomes more complex, and requires additional features to be implemented. Using Nx to implement those boundaries With Nx, and the CLI it comes with, I do recommend creating separate libraries within the monorepo to emphasize the boundaries between modules of the application. In the previous article, I introduced the concept of tags used by Nx to enforce boundaries between different types of libraries. We can use this same set of tools to create the vertical decomposition as well. It is a good practice to create a common prefix for tags that concern the same axis of decomposition. In the case of vertical splitting, I suggest using e.g. scope or domain prefixes. By applying this prefix to the modules defined above, we can create the following tags: - scope:photos - scope:user - scope:chat or - domain:photos - domain:user - domain:chat Similarly to the horizontal type: tags we can not assign the tags defined above to the libraries we've created for specific submodules of the application: ` nx.json And also the boundaries between scopes can be enforced using ESLint or TSLint rules. ` .eslintrc.json I recommend limiting access to only the same scope as a starting point, and enabling access to a different scope only when it is actually necessary. This way we are forced to stop and consider the connection we are about to create, and therefore we can take some time to decide whether that's the best approach. It can lead us to finding and extracting a separate scope that can be used by both current scopes. To verify that the boundaries between libraries are not violated, the following command can be run: ` Of course, the CI process should be set up to make sure that as the codebase evolves the constraints are still met. Conclusion As I have shown in the above sections, the vertical decomposition can greatly benefit the maintainability of the application code. It is especially useful when working with large codebases as they are the ones that probably contain multiple scopes/domains that can be extracted and separated. However, I encourage you to try this approach even on a smaller project as it will be much easier to grasp on a smaller scale. With Nx tools, it is very easy to set up the boundaries between application scopes, and makes sure that those constraints are kept as the application grows. If you want to read more about the architecture in an Nx monorepo, I recommend the following articles: - Semantic Grouping Folders with Nx - Tactical DDD with monorepos In case you have any questions, you can always tweet or DM me @ktrz. I'm always happy to help!...

Decomposing a project using Nx - Part 1 cover image

Decomposing a project using Nx - Part 1

Decomposing a project using Nx - Part 1 Working on a large codebase brings multiple challenges that we need to deal with. One of them is how to manage the repository structure and keep it as clean and maintainable as possible. There are multiple different factors that can be considered when talking about project maintainability, and one of them, that is fundamental in my opinion, is how we structure the project. When it comes to managing large scale project which may consist of many modules, or even separate applications, a Nx Workspace based mono-repository is a good candidate for managing such a project. If you don't know what an Nx Workspace is, I encourage you to read my previous article where I introduce it along with monorepo fundamentals. In this article series, I'll show you: - 2 approaches for decomposing a project - How they can help you to better manage your project's codebase - What tools Nx Workspace provides us with that help us enforce boundaries within a project Modules vs libraries It is a well-known good practice, especially when working with a complex Web Application, to divide the functionality into separate, self-contained, and, when possible, reusable modules. This is a great principle and many modern CLIs (ie. Angular, Nest) provide us with tooling for creating such modules with ease, so we don't waste time creating additional module structure by hand. Of course, we could take it a step further, and instead of just creating a separate module, create a whole separate library instead. This seems to be a bit of an overkill at first, but when we consider that Nx CLI provides us with just as easy a way of creating a library as we did for a module, it doesn't feel so daunting anymore. With that in mind, let's consider what the benefits of creating a separate library instead of just a module are: - libs may result in faster builds - nx affected command will run the lint, test, build, or any other target only for the libraries that were affected by a given change - with buildable libs and incremental builds, we can scale our repo even further - libs enable us to enforce stricter boundaries - code sharing and minimizing bundle size is easier with libs - we can extract and publish reusable parts of our codebase - with small and focused libraries we only import small pieces into the application (in the case of multi-app monorepo) Decomposition strategies - horizontal In this article, I want to focus on horizontal decomposition strategy, which is great not only for large, enterprise projects, but for smaller applications as well. Horizontal decomposition focuses on splitting the project into layers that are focused on a single technical functionality aspect of the module. A good example of libraries type in this case is: - application layer - feature layer - business logic layer - api/data access layer - presentational components layer As you may see in this example layering concept, each of the library types has a specific responsibility that can be encapsulated. I have created an example application that demonstrates how the aforementioned decomposition can be applied into even a simple example app. You can find the source code on my repository. Please check out the post/nx-decomposition-p1 branch to get the code related to this post. This application allows a user to see a list of photos and like or dislike them. It is a very simple use case, but even here, we can distinguish few layers of code: - photo-fe - frontend application top layer - photo-feature-list - this is a feature layer. It collects data from data-access layer, and displays it using ui presentational components. - photo-data-access - this is a layer responsible for accessing and storing the data. This is where we include calls to the API and store the received data using NgRx store. - photo-ui - this library contains all the presentational components necessary to display the list of photos - photo-api-model, photo-model - those are libraries that contain data model structure used either in the API (it's shared by FE and BE applications), and the internal frontend model. API and internal models are the same now but this approach gives us the flexibility to, for example, stop API breaking changes from affecting the whole FE application. To achieve this we could just convert from API to internal model, and vice-versa. This application decomposition allows for easier modifications of internal layer implementation. As long as we keep the interface intact, we can add additional levels of necessary logic, and not worry about affecting other layers. This way we can split responsibility between team members or whole teams. Nx workspace comes with a great toolset for managing dependencies between the internal libraries. A great starting point to get a grasp of the repository structure is to visualize the repository structure, and its dependencies. The following command will show us all libraries within a monorepo and dependencies between those libraries: ` It will open a dependency graph in a browser. From the left side menu, you can choose which projects you want to include in the visualization. After clicking Select all, you should see the following graph: You can read more about dependecy graph here: - Analyzing & Visualizing Workspaces - nx dep-graph - documentation Enforce boundaries As you may see in the dependency graph above, our application layer is accessing only certain other parts/libraries. As the project grows, we would probably like to make sure that the code still follows a given structure. I.e. we would not like UI presentational components to access any data access functionality of the application. Their only responsibility should be to display the provided data, and propagate user's interactions via output properties. This is where Nx tags comes in very handy. We can assign each library its own set of predefined tags, and then create boundaries base on those tags. For this example application, let's define the following set of tags: - type:application - type:feature - type:data-access - type:ui - type:model - type:api-model - type:be Now, within the nx.json file, we can assign those tags to specific libraries to reflect its intent: ` Now that we have our tags defined, we can use either an ESLint or TSLint rule provided by Nrwl Nx to restrict access between libraries. Those rules are named @nrwl/nx/enforce-module-boundaries and nx-enforce-module-boundaries for ESLint and TSLint respectively. Let's define our allowed libraries anteriactions as follows: - type:application - can only access type:feature libraries - type:feature - can only access type:data-access, type:model, type:ui libraries - type:data-access - can only access type:api-model, type:model libraries - type:ui - can only access type:ui, type:model libraries - type:model - can not access other libraries - type:api-model - can not access other libraries - type:be - can only access type:api-model libraries To enforce those constraints we can add each of the rules mentioned above to the @nrwl/nx/enforce-module-boundaries, or nx-enforce-module-boundaries configuration. Let's open the top level .eslintrc.json or .tslint.json files, and replace the default configuration with the following one: ` For type:model and type:api-model, we can either not include any configuration, or explicitly add configuration with an empty array of allowed tags: ` Now, you can run the following command to verify that all constraints are met: ` You can set up the CI to run this check for all the PRs to the repository, and therefore, avoid including code that does not follow the architectural pattern that you decided for your project. If any of the aforementioned constrains were violated, the linting process would produce an error like this ` This gives a clear message on what the problem is, and tells the developer that they are trying to do something that should not be done. You can read more about Nx tags & constraints in the documentation. Conclusion When designing a software solution that is expected to grow and be maintained for a long time, it is crucial to create an architecture that will support that goal. Composing an application out of well-defined and separated horizontal layers is a great tool that can be applied to a variety of projects - even the smaller ones. Nx comes with a built-in generic mechanism that allows system architects to impose their architectural decisions on a project and prevent unrestrained access between libraries. Additionally, with a help of Nx CLI, it is just as fast and easy to create new libraries as with creating a new module. So why not take advantage of it? In case you have any questions, you can always tweet or DM me @ktrz. I'm always happy to help!...

Nx Workspace with Angular and Nest cover image

Nx Workspace with Angular and Nest

Nx Workspace with Angular and Nest In a previous article, we covered creating an Angular project with Nx monorepo tooling. This gives us a great base, but usually, our application will need a server-side project to feed our frontend application with all of the necessary data. Why not leverage the monorepo approach for this use-case then? In this article, I would like to show you how to bring Nest server-side application that will serve our frontend application all the necessary data and behaviors. We will build on top of the existing Nx-based Angular application, which you can find in this GitHub repository. If you want to follow the code in this article, I recommend cloning this repository and checking out new branch with the nxAngularNest_entryPoint tag. ` The application in the aforementioned repository contains a simple application that displays a list of photos that can be either liked or disliked. If you run the code initially, you'll notice that the app requires a backend server from which to pull the necessary data. We will build this simple backend application using the Nest framework, and all of that within a single monorepo project, so that it is easier to manage both applications. Nest Overview Nest is a backend framework for building scalable Node applications. It is a great tool for Angular devs to get into server-side development as it is based on concepts that are very similar to Angular ones: - TypeScript support - Dependency Injection mechanism that is very similar to the Angular mechanism - Puts emphasis on testability - Configuration is similar (mostly based on decorators) - Best practices and conventions are similar - knowledge is transferable All of this makes for a great candidate to use Nest as a server application framework for our application. Let's add a Nest application to our existing project. Add Nest app To start off, we need to install all of the dependencies which will allow Nx to assist us with building a Nest application. All of this is packed into a single Nx plugin @nrwl/nest. ` With the tooling in place, we can generate the Nest application with one command. ` Please keep in mind that, since we're keeping applications using 2 separate Nx plugins, we need to specify the full path to the schematics for generating applications/libraries. In this case, it is @nrwl/nest:application A nice feature when creating a Nest application is the ability to set up a proxy to our newly created application so that our FE application can easily access it. We can use the --frontendProject additional param to do so. Let's use it to create our actual Nest application: ` This command will generate a project skeleton for us. The application is bootstrapped similarly to an Angular app. We define an AppModule, which will be a root of the app, and all the other necessary modules will be imported within this module. ` ` For a more in-depth explanation of the Nest framework, please visit the official docs. Building the API For our photos application we require 3 following endpoints to be handled: GET /api/photos - which returns the list of all photos PUT /api/photos/:photoId/like - allows us to like a photo PUT /api/photos/:photoId/dislike - allows us to dislike a photo To handle requests in Nest, we use a class called Controller which can handle requests to a specific sub-path (in this case it will be the photos sub-path). To keep our application clean, let's create a separate module that will contain our controller and all the necessary logic. `` nx g @nrwl/nest:module app/photos --project=api-photos nx g @nrwl/nest:controller app/photos --project=api-photos --export `` Since the controller shouldn’t contain business logic, we will also create a service to handle the logic for storing and manipulating our photo collection. `` nx g @nrwl/nest:service app/photos --project=api-photos `` Our newly created service will be added to our PhotosModule providers. ` Just like in Angular, we also need to include our PhotosModule in the AppModule's imports to notify Nest of our module's existence. ` Now, we are ready to build the API we need. We can start with the first endpoint for getting all the photos: GET /api/photos Let's start by creating all the necessary logic within the PhotosService class. We need to store our collection of photos and be able to return them in a form of an Array. To store it, I prefer to use an id-based map for quick access. ` To simplify transformation from a map to an array, I added a utility function stateToArray. It can definitely be extracted to a separate file/directory as an application grows, but for now, let's leave it here inline. Now, our controller can leverage this getPhotos function to return a list of all photos via an API. To create an endpoint in Nest, we use decorators corresponding to an HTTP method that we want to expose. In our case, it will be a GET method so we can use a @Get() decorator: ` Now, we can run both our frontend and backend server to see the list of photos requested via our new API. ` ` We still need to implement the liking and disliking feature in the Nest app. To do this, let's follow the same approach as we did earlier. First, let's add the liking functionality to PhotosService: ` and similarly, we can implement the dislike functionality ` With both methods in place, all that is left to do is implement to endpoints in the PhotosController and use methods provided by a PhotosService: ` The path params are defined analogously to how we define params in Angular routing with the : prefix, and to access those params we can use @Param() decorator for a method's parameter. Now, after our server reloads, we can see that the applications are working as expected with both the liking and disliking functionalities working. Common interfaces In this final section, I would like to show you how we can benefit from the monorepo approach by extracting the common interface between the frontend and backend to a separate library. Let's start by creating a library, again using the Nx command tools. `` nx g @nrwl/workspace:library photo/api `` This will generate a new library under libs/photo/api/ folder. Let's create a new file libs/photo/api/src/lib/photo.model.ts and put the ApiPhoto interface in it so it can be shared by both frontend and backend applications. ` We need to export this interface in the index.ts file of the library as well: ` There is no way we can use the same interface for an API request in both of our applications. This way, we make sure that the layer of communication between our applications in always up to date. Whenever we change the structure of the data in our server application, we will have to apply the appropriate changes to the frontend application as well as the TypeScript compiler. This forces data to be consistent and braking changes to be more manageable. Conclusion As you can see, maintaining the project in a monorepo makes it easier to maintain. Nest framework is a great choice for a team of developers that are acquainted with Angular as it builds on top of similar principles. All of that can be easily managed by the Nx toolset. You can find the code for this article's end result on my GitHub repo. Checkout the nxAngularNest_ready tag to get the up-to-date and ready-to-run solution. To start the app you need to serve both Angular and Nest projects: ` ` In case you have any questions you can always tweet or DM me @ktrz. I'm always happy to help!...

Introduction to building an Angular app with Nx Workspace cover image

Introduction to building an Angular app with Nx Workspace

# Introduction to building an Angular app with Nx Workspace Nx Workspace is a tool suite designed to architect, build and manage monorepos at any scale. It has out-of-the-box support for multiple frontend frameworks like Angular and React as well as backend technologies including Nest, Next, and Express. In this article, we will focus on building a workspace for an Angular-based project. Monorepo fundamentals The most basic definition of a monorepo is that it is a single repository that consists of multiple applications and libraries. This all is accompanied by a set of tooling, which enables us to work with those projects. This approach has several benefits including: - shared code - it enables us to share code across the whole company or organization. This can result in code that is more DRY as we can reuse the common patterns, components, and types. This enables to share the logic between frontend and backend as well. - atomic changes - without the monorepo approach, whenever we need to make a change that will affect multiple projects, we might need to coordinate those changes across multiple repositories, and possibly by multiple teams. For example, an API change might need to be reflected both on a server app and a client app. With monorepo, all of those changes can be applied in one commit on one repository, which greatly limits the coordination efforts necessary - developer mobility - with a monorepo approach we get one consistent way of performing similar tasks even when using multiple technologies. The developers can now contribute to other teams' projects, and make sure that their changes are safe across the whole organization. - single set of dependencies - By using a single repository with one set of dependencies, we make sure that our whole codebase depends on one single version of the given dependency. This way, there are no version conflicts between libraries. It is also less likely that the less used part of the repository will be left with an obsolete dependency because it will be updated along the way when other parts of the repository do this update. If you want to read more about monorepos, here are some useful links: - (Monorepo in Git)[https://www.atlassian.com/git/tutorials/monorepos] - (Monorepo != monolith)[https://blog.nrwl.io/misconceptions-about-monorepos-monorepo-monolith-df1250d4b03c] - (Nrwl Nx Resources)[https://nx.dev/latest/angular/getting-started/resources] Create a new workspace With all that said about the monorepo, how do we actually create one using Nx Workspace and Angular? Just like with Angular CLI, there is an Nx CLI that does all the heavy lifting for us. With the following command, we can create a new workspace that leverages all of the aforementioned benefits of a monorepo: ` The tool will ask for a project name, stylesheet format, and linting tool. For the linting, I recommend using ESLint, which is a more modern tool. The CLI will also ask whether we want to use Nx Cloud in our workspace. We can opt-out from this for now as we can easily add that later on. After the command finishes, we end up with a brand new project all set up. Let's start by analyzing what has been generated for us. Nx uses certain toolset by default: - Jest for testing (instead of Karma and Jasmine) - Cypress for e2e testing (instead of Protractor) - ESLint for linting (instead of TSLint) in case you decide to use it when creating a workspace All of these are modern tools, and I recommend sticking to them as they provide very good developer experiences, and are actively maintained. The base structure that is created for us looks as follows: ` - apps/*: here go all the application projects - by default, it'll be the app we created and an accompanying e2e tests app - libs/*: where all of the libraries that we create go - tools/*: here, we can put all of the necessary tooling scripts etc that are necessary in our project - and all the root configuration files like angular.json, config files for Jest, ESLint, Prettier, etc This whole structure is created for us so that we can focus on building the solution right from the beginning. Migration from an existing Angular project If you already have an existing Angular app that was built using the Angular CLI, you can still easily migrate to an Nx Workspace. A project that contains only a single Angular app can be migrated automatically with just one command: ` This will install all of the dependencies, required by Nx, and create the folder structure mentioned in the previous section. It will also migrate the app into apps folder and e2e suite into apps/{{appName}}-e2e folder. Nx modifies package.json script, and decorates Angular CLI so you can still use the same commands like ng build, ng serve, or npm start. It is important to remember that the version of Angular and Nx must match so that this process goes smoothly. For example, if your project is using version 10 of Angular, please make sure to use the latest 10.x.x version of Nx CLI. In case you already have multiple projects, you still can migrate with few manual steps described in the Nx docs. Nx CLI In the following sections, we will use Nx CLI to simplify performing operations on the monorepo. You can install it globally by running one of the following commands: ` ` If you don't want to install a global dependency, you can always invoke local nx via either ` or ` Create a library One of the core ideas behind the Nx Workspace monorepo approach is to divide our code into small, manageable libraries. So by using Nx, we will end up creating a library often. Luckily, you can do this by typing one command in the terminal: ` This will create a libs/mylib folder with the library set up so we can build, test, and use it in other libraries or applications right away. To group the libraries you can use the --directory={{subfolderName}} additional parameter to specify a subfolder under which a library should be created. You don't have to worry about choosing the perfect place for your library from the start, though. You can always move it around later on using @nrwl/workspace:move schematics, and you can find all the other options for generating a new Angular library in the official docs. Every library has an index.ts file at its root, which should be the only access point to a library. Each part of the library that we want to be part of the lib's public API should be exported from this file. Everything else is considered private to the library. This is important for maintaining the correct boundaries between libraries and applications, which makes for more well-structured code. Affected One of the greatest things about Nx Workspace is that it understands dependencies within the workspace. This allows for testing and linting only the projects that are affected by a given change. Nx comes with a few built-in commands for that. ` Those commands will run lint, test, e2e, and build targets, but only on projects that are affected, and therefore they will lower the execution time by a lot in most use-cases. The commands below are equivalent to the ones above, but they use more generic syntax, which can be extended to different targets if necessary. ` For all of the commands mentioned above, we can parallelize them by using --parallel flag and --maxParallel={{nr}} to cap the number of parallel tasks. There are multiple additional useful parameters that the affected task can take. Please visit the official docs for more details. Conclusion Working with a monorepo has a lot of advantages, and Nx Workspace provides us with multiple tools to get the most of that. By using it, we can speed up our development loop by being able to create atomic changes to the repository, and make sure that the whole workspace is compatible with that change. All of this is done with blazing fast tooling that can be scaled to any project size we might have. In case you have any questions, you can always tweet or DM me @ktrz. I'm always happy to help!...

Let's innovate together!

We're ready to be your trusted technical partners in your digital innovation journey.

Whether it's modernization or custom software solutions, our team of experts can guide you through best practices and how to build scalable, performant software that lasts.

Prefer email? hi@thisdot.co