In the ever-evolving world of web development, the tools we rely on for code quality and consistency must adapt and improve. For years, ESLint has been the cornerstone of JavaScript and TypeScript linting, helping teams enforce coding standards and catch errors before they reach production. Recently, the ESLint team introduced a monumental shift that promises to redefine how we configure and manage our linting rules: the Flat Config system. This change, signaled by the new eslint.config.js
file, is more than just a new format; it’s a fundamental rethinking of configuration that brings simplicity, explicitness, and power to developers.
This major update is a significant piece of ESLint News, impacting the entire ecosystem. Whether you’re working with the latest Next.js News, building complex applications with React, or exploring developments in the Vue.js News and Svelte News communities, understanding this new system is crucial. It simplifies integration with modern tools like Vite and build systems like Turbopack, making it easier to maintain high-quality code across any project. In this comprehensive guide, we’ll explore the core concepts of ESLint’s flat config, walk through a practical migration, uncover advanced techniques, and discuss best practices to help you master the future of JavaScript linting.
Why the Change? Unpacking the Motivation Behind Flat Config
To fully appreciate the new flat config system, it’s important to understand the limitations of its predecessor, the `.eslintrc.*` file format. The legacy system, while powerful, often led to complexity and confusion, especially in large-scale projects.
The Limitations of the Legacy .eslintrc
System
The traditional `.eslintrc.js` (or JSON/YAML) configuration relied on a “cascading” hierarchy. ESLint would search for configuration files from the linted file’s directory up to the root of the filesystem. This behavior, combined with properties like extends
and overrides
, created a system with several pain points:
- Implicit Complexity: It was often difficult to determine which configuration or rule was being applied to a specific file due to the cascading and merging logic. Debugging configuration issues could become a time-consuming task.
- Plugin Resolution “Magic”: The way plugins were loaded was implicit. Simply adding a plugin name to an array (e.g.,
"plugins": ["react"]
) relied on ESLint’s internal resolution to find theeslint-plugin-react
package. This lacked the explicitness of standard JavaScript modules. - Complex Overrides: While the
overrides
key was powerful, nesting configurations for different file types within a single file could lead to deeply nested and hard-to-read structures. - Inconsistent File Handling: The separation of configuration in
.eslintrc
and ignore patterns in.eslintignore
felt disjointed.
This complexity was a common topic in discussions around tooling, from Node.js News to the frontend framework communities like Angular News and Remix News. The ecosystem was ready for a more modern, straightforward approach.
Introducing eslint.config.js
: The Core Concepts
The flat config system addresses these challenges by adopting a simpler, more explicit model. Instead of a complex object with special properties, an eslint.config.js
file exports an array of configuration objects. Each object in the array acts as an independent configuration segment that is applied explicitly to the files you define.
Here are the key benefits:
- Explicitness: Everything is explicit. You import plugins and parsers like any other JavaScript module. There’s no more “magic” resolution.
- Simplicity: The configuration is a flat array. To apply different rules to different files, you simply add another object to the array with a
files
glob pattern. This eliminates the need for complexoverrides
nesting. - Centralized Control: Ignore patterns are now handled directly within the configuration file via an
ignores
property, keeping all linting logic in one place. - Programmatic Power: Since it’s a standard JavaScript module, you can use logic, variables, and helper functions to construct your configuration array dynamically, making it incredibly powerful and maintainable.
Let’s compare a simple legacy config with its new flat config equivalent.
Legacy .eslintrc.js
Example:
module.exports = {
env: {
browser: true,
es2021: true,
node: true,
},
extends: [
'eslint:recommended',
'plugin:react/recommended',
'plugin:@typescript-eslint/recommended',
],
parser: '@typescript-eslint/parser',
parserOptions: {
ecmaFeatures: {
jsx: true,
},
ecmaVersion: 'latest',
sourceType: 'module',
},
plugins: ['react', '@typescript-eslint'],
rules: {
'react/react-in-jsx-scope': 'off',
},
settings: {
react: {
version: 'detect',
},
},
};
Modern eslint.config.js
Example:
import globals from 'globals';
import tseslint from 'typescript-eslint';
import pluginReact from 'eslint-plugin-react';
export default [
{
files: ['**/*.{js,mjs,cjs,ts,jsx,tsx}'],
languageOptions: {
parser: tseslint.parser,
parserOptions: {
ecmaFeatures: { jsx: true },
},
globals: {
...globals.browser,
...globals.node,
},
},
plugins: {
'@typescript-eslint': tseslint.plugin,
react: pluginReact,
},
rules: {
...tseslint.configs.recommended.rules,
...pluginReact.configs.recommended.rules,
'react/react-in-jsx-scope': 'off',
},
settings: {
react: {
version: 'detect',
},
},
},
];
The new version is more verbose but infinitely more transparent. You can see exactly where every piece of the configuration, from the parser to the rules, is coming from.
Getting Started: A Step-by-Step Migration Guide
Migrating to the flat config system is a straightforward process. It’s becoming the default in new projects created with tools like Vite, and is a hot topic in Vite News and TypeScript News. For existing projects, especially those using frameworks like Next.js, Nuxt.js, or SolidJS, the transition brings immediate benefits in clarity and maintainability.
Initial Setup and Dependencies
First, ensure you are using a compatible version of ESLint (v8.21.0+ for opt-in, v9+ for default). Then, you can begin by creating an eslint.config.js
file in your project’s root directory. If you are on ESLint v8, you may need to set an environment variable to enable it: ESLINT_USE_FLAT_CONFIG=true
.
Next, you’ll need to update your plugins. Many popular plugins have been updated to support flat config. For a TypeScript project, you’ll want the latest versions:
npm install eslint typescript typescript-eslint eslint-plugin-react@latest --save-dev
Building Your First eslint.config.js
Let’s build a practical configuration for a modern React and TypeScript project. This setup will include recommended rules, globals, and parser settings.
The core idea is to build an array of configuration objects. We’ll start with a global ignore pattern, then add a configuration for our main source files.
// eslint.config.js
import globals from 'globals';
import tseslint from 'typescript-eslint';
import reactPlugin from 'eslint-plugin-react';
import reactRecommended from 'eslint-plugin-react/configs/recommended.js';
import js from '@eslint/js';
export default [
// 1. Global ignores
{
ignores: ['dist/**', 'node_modules/**', '.DS_Store'],
},
// 2. Base configuration for all JS/TS files
js.configs.recommended,
...tseslint.configs.recommended,
// 3. Configuration for React files
{
files: ['src/**/*.{js,jsx,ts,tsx}'],
...reactRecommended,
languageOptions: {
...reactRecommended.languageOptions,
globals: {
...globals.browser,
},
},
plugins: {
react: reactPlugin,
},
rules: {
'react/react-in-jsx-scope': 'off',
'react/prop-types': 'off',
},
},
];
Let’s break this down:
- Global Ignores: The first object in the array uses the
ignores
key. This replaces the need for a separate.eslintignore
file and applies to the entire linting process. - Base Configurations: We use pre-built configurations like
js.configs.recommended
from@eslint/js
and spread the array fromtseslint.configs.recommended
. This is the new, explicit way of “extending” configurations. - React-Specific Configuration: We add a new object specifically for our React component files using a glob pattern in the
files
key. We merge in the recommended React config and then add our own custom rules, such as turning offreact-in-jsx-scope
, which isn’t needed with modern React runtimes. This granular control is a huge advantage.
Mastering Flat Config: Advanced Patterns and Integrations
The true power of flat config shines when you move beyond basic setups. Its programmatic nature allows for clean, sophisticated configurations that are easy to manage, which is great news for developers using a wide range of tools from Express.js News on the backend to Three.js News on the frontend.
Handling Different File Types with Glob Patterns
A common requirement is to apply different rules to different parts of a project, such as source code, tests, and configuration files. With flat config, this is incredibly clean.

Imagine you’re using Vitest for testing, a topic often covered in Vitest News. You’ll want to enable test-specific rules and globals only for your test files.
// eslint.config.js
import globals from 'globals';
import tseslint from 'typescript-eslint';
import pluginVitest from 'eslint-plugin-vitest';
// ... other imports
export default tseslint.config(
// Base configs from previous example...
{
// ... your main source file config
},
// Configuration for test files
{
files: ['**/*.test.ts', '**/*.spec.tsx'],
plugins: {
vitest: pluginVitest,
},
rules: {
...pluginVitest.configs.recommended.rules,
// Custom test-specific rules
'vitest/expect-expect': 'error',
},
languageOptions: {
globals: {
...globals.vitest,
},
},
}
);
In this example, we add a new object to our configuration array. The files
key targets only files ending in .test.ts
or .spec.tsx
. Inside this object, we enable the Vitest plugin, apply its recommended rules, and add the Vitest-specific globals. This configuration segment will not affect any other file in your project, providing perfect isolation.
Integrating with Prettier and Other Tools
One of the most common integrations is with Prettier, the opinionated code formatter. The goal is to have ESLint handle code quality rules and Prettier handle formatting, without them fighting over the same concerns. This has always been a key topic in Prettier News.
With flat config, this integration is simpler than ever. The eslint-config-prettier
package disables all ESLint rules that conflict with Prettier’s formatting.
To use it, first install it: npm install --save-dev eslint-config-prettier
.
Then, add it as the very last item in your configuration array. This ensures it overrides any conflicting rules from previous configurations in the array.
// eslint.config.js
import prettierConfig from 'eslint-config-prettier';
// ... other imports
export default [
// ... all your other configurations
// This must be the last configuration in the array
prettierConfig,
];
That’s it. No more complex extends arrays. This streamlined approach benefits everyone, whether they’re following Cypress News for E2E testing or Electron News for desktop app development, as it ensures a consistent and conflict-free developer experience.
Best Practices and Common Pitfalls
As you adopt the new flat config system, keeping a few best practices in mind will help you create a robust and maintainable linting setup. This is essential for teams working on complex projects with modern bundlers and tools discussed in Webpack News and Rollup News.
Common Migration Hurdles
- Plugin Compatibility: The biggest hurdle is ensuring your plugins support flat config. Most major plugins have been updated, but some smaller or older ones may not have been. Always check the plugin’s documentation for a flat config-compatible export.
- Thinking in Arrays, Not Extends: The mental model has shifted from a single, merged configuration object to a pipeline of configuration objects. Remember that order matters—configurations later in the array can override earlier ones, which is why
eslint-config-prettier
must go last. - Goodbye,
.eslintignore
: Don’t forget to move your ignore patterns from.eslintignore
into theignores
property within youreslint.config.js
. This centralizes all your linting logic.
Keeping Your Linter Configuration Maintainable
For very large projects, a single eslint.config.js
file can still become unwieldy. Since it’s just a JavaScript module, you can leverage standard programming practices to keep it clean:
- Split Configurations: Break down your configuration into logical files (e.g.,
eslint.react.js
,eslint.tests.js
) and import them into your maineslint.config.js
. - Use Helper Functions: Create functions to generate repetitive configuration blocks. This is especially useful if you have multiple applications in a monorepo that share a similar base configuration but have minor differences.
- Comment Liberally: Document why a specific rule is enabled or disabled. The explicitness of flat config, combined with good comments, makes your setup self-documenting.
Conclusion: Embracing the Future of Linting
ESLint’s flat config system is a transformative update that modernizes one of the most essential tools in the JavaScript developer’s toolkit. By moving away from implicit, cascading configurations to an explicit, modular, and programmatic approach, eslint.config.js
offers unparalleled clarity, control, and maintainability. This change aligns perfectly with the broader ecosystem’s shift towards explicit module imports and declarative configurations, a trend seen across the latest Node.js News and in tools like Vite and Turbopack.
While the migration requires a shift in mindset, the long-term benefits are undeniable. Your linting setup will be easier to debug, simpler to extend, and more powerful than ever before. Whether you are starting a new project with Next.js, maintaining a legacy application, or contributing to the open-source community, now is the perfect time to adopt this new standard. Start by converting a small project today, and experience firsthand the clarity and confidence that ESLint’s flat config brings to your codebase.