Intro
If your monolith JS/TS application is growing and you're afraid of losing control, this article might be for you.
Problem
Everyone saw something like this:
src/
components/
hooks/
storage/
In smaller projects, it works but with bigger one, it might be annoying. Let's separate the first module from that:
src/
components/
hooks/
storage/
modules/
moduleA/
components/
hooks/
storage/
Nice.
All of the logic responsible for moduleA
is in one place and the rest of the categories are little bit smaller. So the natural next step will be moduleB
:
src/
...
modules/
moduleA/
moduleB/
Nice?
Not really, modular monolith
is not about the arrangement of files, it's about hard separation. ModuleA
should be independent of moduleB
. It this case: (one repo and one app) files from moduleA
may import stuff from moduleB
(and otherwise).
Boundries
Let's apply eslint-boundries plugin.
yarn add eslint-plugin-boundaries @typescript-eslint/parser @typescript-eslint/parser --dev
// eslintrc
{
"plugins": [
"boundaries"
]
}
Then we have to define elements
:
// eslintrc
{
"settings": {
"boundaries/elements": [
{
"type": "module-a",
"pattern": "src/modules/moduleA/**",
"mode": "file"
},
{
"type": "module-b",
"pattern": "src/modules/moduleB/**",
"mode": "file"
},
]
}
}
All of the files from moduleA
are tagged as module-a
and moduleB
=> moduleA
Rules
Now we're able to prevent moduleA
from importing from moduleB
:
// eslintrc
{
"rules": {
"boundaries/element-types": [2, {
"default": "allow",
"rules": [
{
"from": "module-a",
"disallow": ["module-b"]
},
{
"from": "module-b",
"disallow": ["module-a"]
},
]
}]
}
}
Now when you try to an illegal import eslint will throw an error: Importing elements of type 'moduleA' is not allowed in elements of type 'module-b'. Disallowed in rule 1
import { } from '../moduleA/fileA';
// Importing elements of type 'moduleA' is not allowed in elements of type 'module-b'. Disallowed in rule 1
export const bFn = () => {}
Exceptions
What about the case when moduleA
needs to import some constant value, like name or URL? Eslint will throw an error, however, this behavior is not against the modular monolith approach. Creating copy of these constants also doesn't sound like a good idea.
In my projects always exists one exception:
{
"boundaries/ignore": ["**/*.const.ts"]
}
*.const.ts
files contain only constant values (no classes of functions). Also, not-primitive, exported objects have forced immutability by as const syntax.
Full example on my sandboxes repo link
Conclusion
Eslint-boundaries plugin is a great way to start the migration to modular-monolith approach. But don't rush. Migration from the monolith is not an easy task. You may make a lot of mistakes and that's fine. Don't try to create many elements, and rules at once. Instead, separate one and wait some time. Wrong boundaries are much worse than too few.
Don't fix problems that should not appear. Of course, utils should not import hooks, and hooks shouldn't import components. But there's no need to declare boundaries for that, focus on more important rules like forcing already-defined borders between modules.
Top comments (0)