eslint-plugin-import-helpers exists to do one thing better than its bigger sibling: its order-imports rule lets you define import ordering with arbitrary regex groups, so your convention — framework first, then absolute aliases, then relative — is enforced and autofixed exactly as you designed it. The bigger sibling, eslint-plugin-import, is where the correctness rules live: unresolved paths, dependencies you use but never declared, circular imports. The two plugins are complementary, not competing, and the combination turns import statements from a bikeshedding topic into both a solved formatting problem and a quiet early-warning system for dependency mistakes that have real supply chain consequences. Here is how to configure each, and which rules pay rent.
Two plugins, two jobs
eslint-plugin-import (maintained under the import-js organization) ships dozens of rules across static analysis, helpfulness, and style. Its own import/order rule handles grouping and alphabetization and has grown steadily more configurable via pathGroups.
eslint-plugin-import-helpers (by Will Honey) was created as a supplement when import/order was less flexible than teams needed. Its single rule, order-imports, takes a groups array where each entry is a builtin category (module, parent, sibling, index) or any regex:
// eslint config (flat config, ESLint 9)
const importHelpers = require('eslint-plugin-import-helpers');
module.exports = [
{
plugins: { 'import-helpers': importHelpers },
rules: {
'import-helpers/order-imports': ['warn', {
newlinesBetween: 'always',
groups: [
'/^react/', // react and react-* first
'module', // other node_modules
'/^@company\\//', // internal scoped packages
'/^~\\//', // path alias imports
['parent', 'sibling', 'index'],
],
alphabetize: { order: 'asc', ignoreCase: true },
}],
},
},
];
The regex groups are the differentiator: import/order's pathGroups can approximate this, but import-helpers' model is simpler to reason about when your convention has many tiers. Both rules are autofixable, which is the property that makes ordering rules worth having at all — set to warn, run eslint --fix in the pre-commit hook, and no human ever sorts an import block again. On version support: import-helpers 2.0 added ESLint v9 compatibility, and eslint-plugin-import exposes flatConfigs.recommended for flat config, so both work on current toolchains. (The community fork eslint-plugin-import-x also exists with a faster release cadence and is API-compatible for most rules, if the original's pace becomes a constraint for you.)
Pick one ordering rule. Running import/order and import-helpers/order-imports together produces fix loops where each autofixer reorders the other's output.
The correctness rules are the actual value
Ordering is hygiene. These eslint-plugin-import rules catch bugs, and two of them are quietly security-relevant.
import/no-extraneous-dependencies errors when code imports a package that is not declared in package.json. Undeclared-but-resolvable imports — phantom dependencies — work on your machine because hoisting happened to place the package in reach, then break (or worse, resolve to something else) when an unrelated dependency update reshuffles the tree. In a workspace monorepo, a phantom dependency means your SBOM and vulnerability scans are auditing a manifest that does not match what the code actually loads. Configure it per file class:
'import/no-extraneous-dependencies': ['error', {
devDependencies: ['**/*.test.ts', '**/*.config.js', 'scripts/**'],
optionalDependencies: false,
}],
import/no-unresolved errors when an import path resolves to nothing. Beyond catching renamed-file breakage before runtime, it is your cheapest tripwire against dependency typos: import lodash from 'ladash' fails lint the moment it is written instead of at install time — and install time is exactly when a typosquatted package would execute its install script. Lint cannot replace registry-side protections, but failing fast on unresolvable names shrinks the window where a typo becomes an npm install of something malicious. Pair the lint rule with registry-side checks from an SCA tool like Safeguard, which evaluates the package you did install rather than the name you meant.
import/no-cycle flags circular imports, which manifest as undefined exports at runtime in CommonJS and as subtle initialization-order bugs in ESM. It is expensive — it walks the module graph — so scope it: set maxDepth (a small value catches the cycles that matter), and consider running it only in CI rather than in editors.
import/no-internal-modules and import/no-restricted-paths enforce architecture: which layers may import which. Teams use no-restricted-paths to keep server/ code out of client/ bundles — a genuine data-exposure control, since a casual import of a server constants file can carry secrets or internal endpoints into shipped JavaScript. If you have ever found an API key in a bundle with webpack-bundle-analyzer, this rule is the shift-left version of that discovery.
Resolver configuration: where setups go wrong
Most frustration with eslint-plugin-import traces to the resolver — the component that maps import strings to files. TypeScript paths, workspace packages, and bundler aliases all need the resolver to know about them, or no-unresolved drowns you in false positives (and gets turned off, taking its value with it):
npm install --save-dev eslint-import-resolver-typescript
settings: {
'import/resolver': {
typescript: { project: ['./tsconfig.json', './packages/*/tsconfig.json'] },
node: true,
},
},
The rule of thumb: if no-unresolved reports a path your bundler accepts, fix the resolver settings, never the rule severity. A disabled correctness rule is worse than an absent one, because it implies coverage that is not there.
Performance is the other recurring complaint on large repos. Mitigations that work: enable import/no-cycle and other graph-walking rules in CI only, use ESLint's caching (--cache), and let the TypeScript resolver do the heavy lifting once rather than configuring three resolvers that each re-walk node_modules.
A pragmatic combined setup
For a TypeScript monorepo, the configuration that teams converge on:
eslint-plugin-importwithflatConfigs.recommendedandflatConfigs.typescriptas the base.import/no-extraneous-dependencies,import/no-unresolved,import/no-self-importat error — these are bugs.import/no-cycleat error in CI, off in editors.eslint-plugin-import-helpers/order-importsat warn with autofix in the pre-commit hook — style should never block a human, only a robot.import/orderoff (superseded by import-helpers).
That split — correctness blocks merges, style gets autofixed silently — keeps the lint signal trustworthy. When every violation that reaches a PR review is a real problem, people stop ignoring the linter, which is the whole game. Lint is also the first tier of a defense stack, not the last: it validates the imports you wrote, while dependency scanning validates the packages behind them — the Safeguard Academy covers how those layers hand off to each other if you are building the pipeline out.
FAQ
What is the difference between eslint-plugin-import and eslint-plugin-import-helpers?
eslint-plugin-import is a broad ruleset covering import correctness (unresolved paths, extraneous dependencies, cycles) plus its own ordering rule. eslint-plugin-import-helpers provides one rule, order-imports, with a simpler and more flexible grouping model using arbitrary regexes. Use import for correctness, import-helpers for ordering, and never both ordering rules at once.
Does eslint-plugin-import-helpers work with ESLint 9 flat config?
Yes — version 2.0 added ESLint v9 support, and eslint-plugin-import exposes flatConfigs.recommended/flatConfigs.typescript for flat config setups.
Can import lint rules improve security?
Modestly but genuinely: no-extraneous-dependencies keeps your manifest honest so SBOMs and scanners audit reality, no-unresolved fails fast on typoed package names before anything installs, and no-restricted-paths keeps server-only code (and its secrets) out of client bundles.
Why does import/no-unresolved flag paths that build fine?
The ESLint resolver does not automatically know about TypeScript paths, workspace links, or bundler aliases. Install and configure eslint-import-resolver-typescript (or the node resolver options) so lint resolution matches build resolution — fix the resolver, not the rule.