Linting & Formatting Best Practices
A condensed summary of the 25 most important best practices drawn from every page in this section.
- Use ESLint 9 Flat Config: Adopt
eslint.config.mjsand delete every leftover.eslintrc.*file, because ESLint falls back to the legacy format if both exist and your flat config is silently ignored. - Bridge Presets With FlatCompat: Next.js presets still ship legacy
extends-style configs, so wrap them withFlatCompat({ baseDirectory: __dirname })and recreate__dirnameviafileURLToPath(import.meta.url)in ESM. - Isolate Global Ignores: In flat config,
{ ignores: [...] }only acts as a global ignore when it is the sole key in its object; mixing it withrulesturns it into a per-file filter. - Start From next/core-web-vitals: It bundles
eslint-plugin-react,react-hooks,next,import, andjsx-a11y, so you rarely need to install those plugins separately and can layer only what is missing. - Pick Severity Intentionally: Use
"error"for rules you want to enforce (fail CI, block builds) and reserve"warn"for rules you are migrating toward, since warnings pass CI and silently accumulate. - Prefer the TypeScript no-unused-vars: Turn the base
no-unused-varsoff and enable@typescript-eslint/no-unused-varswithargsIgnorePattern: "^_"so interface/type-alias/enum syntax is understood and conflicts disappear. - Enforce consistent-type-imports: Require
import type { … }for type-only imports so bundlers can fully erase types and your type graph stays clearly separated from your value graph. - Never Disable exhaustive-deps Globally: Keep
react-hooks/exhaustive-depson (usually as"warn") and suppress it per-line with// eslint-disable-next-lineonly for provably stable references likedispatchor refs. - Order Imports With Group Separators: Configure
import/orderwith grouped categories (builtin, external, internal, parent/sibling, index, type),"newlines-between": "always", and alphabetization; runeslint --fixonce and add any missing blank lines by hand. - Don't Re-register Bundled Plugins: Installing a plugin that
next/core-web-vitalsalready includes (react, react-hooks, next, import, jsx-a11y) causes duplicate registration and unexpected rule behavior, so check the preset first. - Scope Test-Only Plugins With files: Gate
eslint-plugin-testing-librarybehindfiles: ["**/*.test.{ts,tsx}", "**/*.spec.{ts,tsx}"]; applied globally it produces false positives on non-test code. - Profile Slow Rules: Run
TIMING=1 npx eslint .when lint times creep up — five or more active plugins can meaningfully slow linting, and type-aware@typescript-eslintrules are 2-5x slower because they require full type info. - Put eslint-config-prettier Last: The
"prettier"entry must be the final item in yourextendsarray, because it only turns off formatting rules and any config listed after it will re-enable conflicts. - Don't Use eslint-plugin-prettier: Running Prettier inside ESLint is slow and floods the editor with red squiggles for pure formatting differences — run ESLint and Prettier as separate commands instead.
- Always Ship a .prettierignore: Without it Prettier walks
.next/,node_modules/, lock files, and generated code; an explicit ignore file keeps runs fast and prevents unintended changes. - List Tailwind Plugin Last:
prettier-plugin-tailwindcssmust sit at the end of the Prettierpluginsarray or it will conflict with other Prettier plugins and class sorting breaks. - Pin endOfLine to lf: Set
"endOfLine": "lf"in.prettierrcand configuregit config --global core.autocrlf inputso Windows contributors do not ship CRLF files that failprettier --checkon Linux CI. - Commit .vscode Team Settings: Check in
.vscode/settings.jsonand.vscode/extensions.json(notlaunch.jsonor*.code-workspace) so every developer inherits the same format-on-save, ESLint, and recommended-extensions behavior. - Set defaultFormatter Per Language: Declare
editor.defaultFormatterboth globally and per language ([typescript],[typescriptreact],[json]) to avoid VS Code picking a random formatter when multiple are installed. - Use the Workspace TypeScript: Point
typescript.tsdkatnode_modules/typescript/libso editor type-checking matches the versiontscuses in CI, eliminating "works on my machine" type drift. - Enable strict Plus Extra Flags: Turn on
"strict": truefor the eight bundled checks and addnoUncheckedIndexedAccess,noImplicitReturns,noFallthroughCasesInSwitch, andverbatimModuleSyntaxbecause they are not included instrict. - Adopt Strict Incrementally: On a legacy codebase, keep
"strict": falseand enablestrictNullChecksfirst, thennoImplicitAny, using// @ts-expect-error(not@ts-ignore) so suppressions auto-fail once the underlying issue is fixed. - Run tsc --noEmit in CI: TypeScript catches cross-file type breaks that ESLint cannot (for example, removing a union member from a shared type); add
"type-check": "tsc --noEmit"and rely onincremental: trueplus a cached.tsbuildinfofor speed. - Install Husky via prepare: Put
"prepare": "husky"inpackage.jsonso pre-commit hooks are installed automatically afternpm install, and keep hooks fast by running onlylint-staged(witheslint --fix --no-warn-ignored) — never fulltsc. - Make CI the Safety Net: Run lint,
prettier --check, andtsc --noEmitin GitHub Actions usingnpm cifor deterministic installs, a matching pinned Node version, aconcurrencygroup withcancel-in-progress: true, and branch-protection rules that require the job to pass before merge.