Split Apps into Libs
Do You Really Need to Split Apps into Libs?
TL;DR: no, but you probably should!
First, let's clarify what we mean by a lib (or library).
Nx libs are not necessarily published to npm or built separately.
They can be just folders with code and generally a single entry point (e.g. index.ts
) exposing what the library needs to expose.
Back to the initial question, tiny apps that are already well organized might not need to be split into libs, but otherwise, splitting apps into libs is a good idea to get the following benefits:
- Clear separation of concerns,
- Faster linting/testing etc... thanks to Nx caching and parallelization,
- Implementation details stay contained in libs (i.e. app code is not polluted with implementation details),
- Lower risk of cyclic dependencies,
- Embracing change (e.g. ready to reuse libs in future apps, or just open-source them whenever needed),
- You can easily apply a linting rule that enforces architectural styles (e.g. Tactical DDD, Hexagonal Architecture, ...),
- You can use different tooling for different libs in order to transition progressively (e.g. transitioning from Jest to Vitest).
Creating Libraries
In order to create a new library, you can use the following command:
nx g lib <library-name>
This will usually prompt you to choose the type of library you want to create (e.g. Angular, React, Nest, Node, ...) depending on the Nx plugins available in your workspace.
? Which generator would you like to use? …
> @nx/angular:library
@nx/js:library
@nx/nest:library
@schematics/angular:library
You can also skip the prompt by simply specifying the generator you want to use: nx g @nx/angular:library <library-name>
If <library-name>
is a path, the library will be created in the specified directory, and the last part of the path will be used as the library name:
nx g @nx/angular:library libs/my-lib
will generate the following files:
libs/my-lib
├── README.md
├── project.json
├── src
│ ├── index.ts
│ ├── lib
│ │ └── my-lib
│ │ └── my-lib.component.ts
├── tsconfig.json
└── ... (other files)
and update the tsconfig.base.json
which is used by all the projects in the workspace, in order to allow importing the library using the @myorg/my-lib
path.
{
...
"compilerOptions": {
...
"paths": {
"@marmicode/my-lib": ["libs/my-lib/src/index.ts"]
}
}
}
You can override this behavior using the --directory
and --importPath
options.
Note that you can also create a library using the Nx Console and its IDE integrations. Cf. https://nx.dev/getting-started/editor-setup
The --dry-run
option allows you to preview the changes before applying them.
Nx Console will also preview the changes while you are creating the library.
To stay on the safe side, make sure to commit or throw away your changes before creating a library. 😉
Non-Buildable vs. Buildable vs Publishable libs
There are three types of libraries in Nx: Non-buildable, Buildable, and Publishable.
- Non-buildable libraries are just a way to organize code within a workspace.
- They are the default behavior of the generators.
- They are meant to be used by other projects in the workspace.
- They are not built separately.
- They are not published to a registry (e.g. NPM).
- Apps using them will use the source code directly.
- Buildable libraries are useful to enable incremental build if needed.
- They are created by passing the
--buildable
option when generating the library. - They are built separately.
- They are not meant to be published to a registry. (e.g. their
package.json
will usually have theprivate
option set to true) - Apps using them should use the built version.
- They are created by passing the
- Publishable libraries are meant to be used inside and outside the workspace.
- They are created by passing the
--publishable
option when generating the library. - They are built separately.
- They are meant to be published to a registry.
- Apps using them should use the built version.
- They are created by passing the
All these libraries are used similarly within the workspace (i.e. using the import path defined in the tsconfig.base.json
: @marmicode/my-lib
).
As non-buildable libraries are not built separately, the build behavior will depend on the configuration of the apps using them.
For example, given the following scenario:
- a workspace with two apps A and B using the same non-buildable library,
- app A has
strictNullChecks
enabled, - app B has
strictNullChecks
disabled.
No matter what's in the local tsconfig.json
of the library itself, the library will be built with strictNullChecks
enabled when building app A, and with strictNullChecks
disabled when building app B.
Moving code to libraries progressively
Moving code to libraries should be done progressively.
We will elaborate on this in a future dedicated chapter as you will need a good strategy to avoid cyclic dependencies hell.
Meanwhile, we dive into a migration strategy later, here is a little trick that will help you quickly move existing code to a library.
-
Move the code you want to the desired library folder. (We recommend using WebStorm for this as it will automatically update the imports in a more reliable way, especially when moving multiple files simultaneously)
-
Export the desired symbols through the library's
index.ts
. (e.g.export * from './lib/my-file.ts';
) -
Run the following command to automagically fix the imports updated by the IDE in the first step:
nx run-many -t lint --fix
This will turn imports like this import { myThing } from '../../../../libs/my-lib/src/lib/my-file';
into import { myThing } from '@marmicode/my-lib';
thanks to the built-in @nx/enforce-module-boundaries
eslint rule.