Moving to ESLint Flat Config: When the Migration Is Worth It
ESLint's flat config replaces the .eslintrc cascade with one explicit JavaScript file. Here's what changes, a worked example, and how to decide when migrating is worth it.
04 Aug 2026, 00:14 UTC

If your project still carries a stack of .eslintrc.js, .eslintrc.json, and per-directory overrides, you've probably hit the classic pain: rules that silently differ between folders, extends chains you have to trace through three packages, and plugins that behave differently depending on which config file ESLint picked up first. Flat config, available since ESLint v8.23.0 and the default in v9, replaces that cascade with a single JavaScript file exporting an array of config objects. The takeaway: migrating is usually worth it for new projects and actively maintained codebases, but check your plugins first — some older ones still only ship legacy-format exports.
What flat config actually changes
The legacy system resolved configuration by walking up the directory tree, merging .eslintrc.* files, and resolving extends through npm package names. It worked, but the resolution order was hard to reason about and hard to debug.
Flat config replaces that with one explicit file — eslint.config.js (or .mjs / .cjs) at your project root — that exports an array. Each entry in the array can declare which files it applies to (via glob patterns), language options, plugins, and rules. Entries are merged in array order, so later objects override earlier ones for the files they match. The cascade is gone; what you see in the file is what runs.
ESLint's CLI detects flat config automatically when eslint.config.js exists, and falls back to legacy config otherwise. On v8 you can also force the new system with the ESLINT_USE_FLAT_CONFIG=true environment variable.
A worked example
Here's a minimal but realistic flat config for a mixed JavaScript project with tests:
// eslint.config.js — run from the project root, no special permissions needed
import js from '@eslint/js';
export default [
js.configs.recommended,
{
files: ['src/**/*.js'],
rules: {
'semi': 'error',
'no-unused-vars': 'warn'
}
},
{
files: ['tests/**/*.js'],
languageOptions: {
globals: { describe: 'readonly', it: 'readonly' }
},
rules: {
'no-unused-expressions': 'off'
}
}
];
The first entry applies a shared baseline everywhere. The second tightens rules for source files. The third relaxes one rule for tests and declares test-runner globals. Because merging is ordered and explicit, you can read top to bottom and know exactly what applies to any file.
Verify it with:
npx eslint --version # flat config needs v8.23.0+; default in v9
npx eslint . # lints using eslint.config.js if present
npx eslint --print-config src/index.js # shows the resolved config for one fileThe --print-config check is the most useful: it prints the fully merged configuration for a specific file, so you can confirm your overrides land where you expect before rolling the change out to CI.
The trade-off: plugin compatibility
The main limitation is the plugin ecosystem. Flat config expects plugins to be imported as modules and referenced directly in the config array, rather than named by string. Many popular plugins ship flat-compatible exports, but some older or less-maintained ones do not. If you depend on one of those, you have three options: wrap it with a compatibility utility (ESLint's @eslint/eslintrc package provides FlatCompat for this), keep that part of your linting on legacy config, or drop the plugin.
Also avoid mixing both systems: if ESLint detects both an eslint.config.js and legacy .eslintrc.* files, behavior depends on version and can produce warnings or silently ignored rules. Migrate fully — delete the old files once the flat config reproduces your rule set.
A practical migration checklist
- Confirm your ESLint version:
npx eslint --version(v8.23.0+ required; v9 makes flat config the default). - List every plugin and shared config in your current
.eslintrc.*files and check each one for flat-config support. - Write
eslint.config.jsalongside the old config, then compare resolved rules per directory with--print-config. - Run
npx eslint .and diff the lint output against your baseline; expect some differences if your old cascade had accidental overrides. - Once output matches (or you've accepted the diffs), remove the legacy files and update CI to the same ESLint version.
Start with one package or repo rather than a whole monorepo at once. The explicit, ordered config model pays for itself quickly: the next time someone asks "why is this rule off in this folder?", the answer is in one file, in order, in plain JavaScript.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.