As software projects grow in size and complexity, maintaining a clean and organized codebase becomes crucial. In large JavaScript and TypeScript codebases, ensuring consistent architectural practices can reduce technical debt and improve maintainability. ESLint's no-restricted-imports
rule offers a powerful tool for enforcing these policies. This article explores how you can leverage this rule to create a cleaner codebase.
ESLint and no-restricted-imports
ESLint is a popular linter for JavaScript and TypeScript, providing a framework for identifying and reporting on patterns found in the code. Among its many rules, no-restricted-imports
allows developers to specify import patterns that should be avoided within the codebase. By configuring this rule, you can:
Prevent the use of specific modules or files: This is useful for deprecating old utilities or avoiding problematic dependencies. Although this is not the focus of this article, you can read more about this in the ESLint Docs.
Enforce architectural boundaries: By restricting imports based on directory structure, you can enforce module boundaries, ensuring that the codebase complies with your architectural policy.
Some examples of such architectural policies include:
- Feature modules should not directly import from other feature modules.
- UI components should not directly access data layer modules, but only through a DAL module.
- Modules should not import anything from a service directory except items exported from
service/index.ts
as service gateway. - Modules inside Services, Controllers, Providers, or any other conceptual components should honor their relations within the codebase.
Let's go over two simplified examples to grasp how it can help in your projects.
Basic Example
Given the following tree, let's say you implemented an internal service with a dozen of interconnected files including classes, functions, variables, etc. Now you do not want other services to be able to import any of the bells and whistles from the service directory. They should only import provided items from the service's API, api.js
.
src
├── main.js
└── internal
├── api.js
├── constants.js
├── bells.js
└── whistles.js
// internal/constants.js
export const Pi = 3.14;
// internal/api.js
import { Pi } from "./constants.js";
export function getPi() {
return Pi;
}
Setting Up ESLint
To get started, you need to have ESLint installed in your project. If you haven't already:
npm i eslint -D
Add a new script to your package.json
for convenience:
"lint": "eslint"
Enforcing Architectural Policies
Next, configure your eslint.config.js
file to include the no-restricted-imports
rule:
export default [
{
rules: {
'no-restricted-imports': [
'error',
{
patterns: [
{
group: ['*/internal/**', '!*/internal/api.js'],
message: 'Do not import from internal service directly. Use the public API instead.'
}
]
}
]
}
}
];
Strings you see in the group
array follow gitignore syntax, allowing you to include and exclude files and directories as needed.
Results
You should expect an error if you import directly from any file in the internal
directory except api.js
.
// main.js
import { Pi } from "./internal/constants.js"; // ❌ Lint should fail
import { getPi } from "./internal/api.js"; // ✅ Lint should pass
Given a non-compliant usage you should get and error when you run npm run lint
:
// main.js
import { Pi } from "./internal/constants.js";
> eslint
src\main.js
1:1 error './internal/constants.js' import is restricted from being used by a pattern.
Do not import internal modules directly. Use the public API instead no-restricted-imports
Hierarchal Architecture Example
For large codebases, it's crucial to enforce architectural boundaries. For example, you might want to ensure that a low-level module does not import from a high-level module. Additionally, you want to ensure that a high-level module does not import directly from a low-level module but through a mid-level module.
Enforcing Architectural Policies
To achieve these policies, we need to use the files
and excludes
properties in ESLint's flat config file and combine them with group patterns in no-restricted-imports
:
hierarchy
├── high
| ├── ui.js
| └── ...
├── mid
| ├── api.js
| └── ...
└── low
├── constants.js
└── ...
export default [
{
ignores: ['src/hierarchy/mid/**/*.js'],
rules: {
'no-restricted-imports': [
'error',
{
patterns: [
{
group: ['**/low/**'],
message: 'Low level modules can only be imported in mid level modules'
},
]
}
]
},
},
{
ignores: ['src/hierarchy/high/**/*.js'],
rules: {
'no-restricted-imports': [
'error',
{
patterns: [
{
group: ['**/mid/**'],
message: 'Mid level modules can only be imported in high level modules'
},
]
}
]
},
},
{
files: ['src/hierarchy/mid/**/*.js', 'src/hierarchy/low/**/*.js'],
rules: {
'no-restricted-imports': [
'error',
{
patterns: [
{
group: ['**/high/**'],
message: 'High level modules can not be imported in low or mid level modules'
},
]
}
]
},
},
];
Results
Here we are exporting 3 instructions with similar rules. In this configuration:
Any attempt to import from any of the files inside the
low
directory will be flagged, except by files in themid
directory.Any attempt to import from any of the files inside the
mid
directory will be flagged, except by files in thehigh
directory.Any attempt by files inside
low
ormid
directory to import from any of the files inside thehigh
directory will be flagged.
Conclusion
Enforcing architectural policies in your codebase is essential for maintaining consistency, reducing technical debt, and ensuring long-term maintainability. ESLint's no-restricted-imports rule is an effective tool to help achieve these goals. By configuring this rule, you can:
- Prevent unwanted dependencies by restricting specific import patterns.
- Ensure that all developers adhere to the defined architectural boundaries.
- Simplify code reviews by automating the enforcement of coding standards.
Involving your development team in defining these rules ensures they are practical and aligned with best practices. Regularly review and update your ESLint configuration as your project evolves to keep your codebase robust and well-organized.
By leveraging ESLint's no-restricted-imports
rule, you can create a cleaner, more maintainable codebase, ultimately improving the overall quality of your software projects.
Let's Talk!
I wrote this article as my first post on DEV, to share a practical approach to maintaining a clean and organized codebase, which is something I've found crucial in my own projects. I hope you find it helpful in your development work. If you have any questions or thoughts, please leave them in the comments. I'd love to hear your feedback and continue the discussion!
Top comments (2)
Thanks for this article, this was very timely! As we're defining the architecture for a new React Native app and updating a legacy React app, I think this pattern will help us organize our JS code and reduce coupling. I appreciate it!
Thank you, Josh! I'm glad to hear the article was helpful. If you have any questions as you implement these techniques, feel free to reach out.
Good luck with your new React apps!