ESLint Setup for Next.js
Configure ESLint with the flat config format for a Next.js project using built-in presets.
Recipe
Quick-reference recipe card — copy-paste ready.
# Install ESLint (included with create-next-app)
npm install --save-dev eslint eslint-config-next
# Run linting via Next.js CLI
npx next lint
# Lint and auto-fix
npx next lint --fix
# Lint specific directories
npx next lint --dir src --dir appWhen to reach for this: Every Next.js project should have ESLint configured from day one.
Working Example
// eslint.config.mjs
import { FlatCompat } from "@eslint/eslintrc";
import { dirname } from "path";
import { fileURLToPath } from "url";
const __filename = fileURLToPath(import.meta.url);
const __dirname = dirname(__filename);
const compat = new FlatCompat({
baseDirectory: __dirname,
});
const eslintConfig = [
...compat.extends("next/core-web-vitals", "next/typescript"),
{
rules: {
// Customize rules here
"@typescript-eslint/no-unused-vars": [
"error",
{ argsIgnorePattern: "^_", varsIgnorePattern: "^_" },
],
"react/no-unescaped-entities": "off",
},
},
{
ignores: [
"node_modules/",
".next/",
"out/",
"public/",
"coverage/",
],
},
];
export default eslintConfig;What this demonstrates:
- ESLint 9 flat config format using
eslint.config.mjs - Using
FlatCompatto bridge Next.js configs (which still use the legacy format) into flat config - Extending
next/core-web-vitals(includes React, hooks, and import rules) andnext/typescript - Customizing a rule and ignoring directories
Deep Dive
How It Works
- Next.js ships with its own ESLint integration via
npx next lint - The
next/core-web-vitalspreset bundleseslint-plugin-react,eslint-plugin-react-hooks,eslint-plugin-next, andeslint-plugin-importwith strict defaults - The
next/typescriptpreset adds@typescript-eslint/eslint-pluginrules tuned for Next.js - ESLint 9 uses flat config (
eslint.config.mjs) instead of the legacy.eslintrcformat FlatCompatis a bridge that lets you use legacyextends-style configs inside flat config — necessary because Next.js presets haven't fully migrated yet
Variations
Standalone ESLint (without next lint):
# Run ESLint directly
npx eslint . --fix
# Or via package.json script{
"scripts": {
"lint": "next lint",
"lint:fix": "next lint --fix",
"lint:strict": "next lint --strict"
}
}Strict mode (treat warnings as errors):
npx next lint --strictIgnoring files:
// In eslint.config.mjs — ignores block
{
ignores: [
"**/*.config.js",
"**/*.config.mjs",
"migrations/",
"generated/",
],
}TypeScript Notes
# The next/typescript preset handles these automatically:
# - @typescript-eslint/parser is configured
# - TypeScript-aware rules are enabled
# - .ts and .tsx files are included by defaultGotchas
Things that will bite you. Each gotcha includes what goes wrong, why it happens, and the fix.
-
Flat config not detected — If ESLint ignores your
eslint.config.mjs, you may have a leftover.eslintrc.*file. ESLint 9 prefers flat config but falls back to legacy if both exist. Fix: Delete all.eslintrc.*files when using flat config. -
First run prompts for config — Running
npx next lintfor the first time shows a setup wizard. Fix: Choose "Strict" fornext/core-web-vitalsor create the config file manually beforehand. -
Ignores must be a separate object — In flat config, the
ignoresarray only works as a global ignore when it is the only key in its config object. Mixingignoreswithrulesin the same object makes it a filter, not a global ignore. Fix: Always put global ignores in their own{ ignores: [...] }object. -
Performance on large projects —
npx next lintonly lintsapp/,pages/,components/,lib/, andsrc/by default. Custom directories need--dir. Fix: Add--dirflags or configureeslint.dirsinnext.config.js.
Alternatives
Other ways to solve the same problem — and when each is the better choice.
| Alternative | Use When | Don't Use When |
|---|---|---|
| Biome | You want a single fast tool for linting and formatting | You need the full ESLint plugin ecosystem |
oxlint | You want extremely fast linting written in Rust | You need custom or Next.js-specific rules |
eslint CLI directly | You need full control over which files are linted | You want Next.js-aware defaults out of the box |
FAQs
Why does Next.js use FlatCompat instead of native flat config?
- Next.js ESLint presets (
next/core-web-vitals,next/typescript) still use the legacyextendsformat internally. FlatCompatbridges these legacy configs into ESLint 9 flat config.- Once Next.js ships native flat config support,
FlatCompatwill no longer be needed.
What is the difference between next/core-web-vitals and next/typescript?
next/core-web-vitalsbundles React, hooks, import, and Next.js-specific rules with strict defaults.next/typescriptadds@typescript-eslint/eslint-pluginrules tuned for Next.js.- Most projects extend both.
Can I use a .eslintrc file instead of eslint.config.mjs?
- ESLint 9 prefers flat config (
eslint.config.mjs). - If both a
.eslintrc.*andeslint.config.mjsexist, ESLint falls back to the legacy file. - Delete all
.eslintrc.*files when migrating to flat config to avoid confusion.
How do I lint directories outside the Next.js defaults?
# Next.js only lints app/, pages/, components/, lib/, src/ by default
npx next lint --dir src --dir utils --dir servicesWhat does --strict mode do in next lint?
- It treats all warnings as errors.
- Any rule set to
"warn"will now fail the lint run. - Useful for CI where you want zero tolerance for warnings.
Gotcha: Why does ESLint ignore my global ignores pattern?
- In flat config,
ignoresonly works as a global ignore when it is the sole key in its config object. - If you mix
ignoreswithrulesin the same object, it acts as a file filter, not a global ignore. - Always put global ignores in a separate
{ ignores: [...] }object.
How do I suppress unused variable errors for function parameters in TypeScript?
"@typescript-eslint/no-unused-vars": [
"error",
{ argsIgnorePattern: "^_", varsIgnorePattern: "^_" },
]Prefix unused parameters with _ and they will be ignored.
What is the purpose of the __filename and __dirname setup in eslint.config.mjs?
- ES modules do not have
__filenameor__dirnameglobals. fileURLToPath(import.meta.url)anddirname()recreate them.FlatCompatrequiresbaseDirectoryto resolve relative config paths.
How do I type-check the ESLint config file itself?
eslint.config.mjsis plain JavaScript, not TypeScript.- You can add
// @ts-checkat the top and use JSDoc annotations for basic type checking. - Alternatively, use
eslint.config.tswith experimental TypeScript config support in ESLint 9.
Gotcha: Why does the first run of npx next lint prompt me to choose a config?
- Next.js shows a setup wizard on first run if no ESLint config file exists.
- To avoid this, create
eslint.config.mjsbefore runningnpx next lint. - Choose "Strict" in the wizard to get
next/core-web-vitalsdefaults.
Can I run ESLint directly instead of through next lint?
# Direct ESLint (skips Next.js directory defaults)
npx eslint . --fix
# next lint (respects Next.js directory defaults)
npx next lint --fixUse npx eslint . when you need full control over which files are linted.
How do I add TypeScript path alias support to ESLint?
- The
next/typescriptpreset configures the TypeScript parser automatically. - Path aliases like
@/*defined intsconfig.jsonare resolved by the parser. - No extra ESLint configuration is needed for path aliases in a Next.js project.
Related
- Essential ESLint Rules — configure individual rules
- ESLint Plugins — add plugins for React, TypeScript, accessibility
- ESLint + Prettier Integration — avoid formatting conflicts
- Linting in CI/CD — run ESLint in GitHub Actions