Set up ESLint and Prettier in a Next.js project — eslint-config-next with Core Web Vitals and TypeScript rules, the flat eslint.config.mjs, wiring Prettier in without conflicts, format-on-save, ignores, and enforcing it in CI.
Why: both are dev dependencies — tools you use while building, never shipped to users. If you scaffolded with create-next-app and chose ESLint, this is already done and you have an eslint.config.mjs — skip to the next step. Setting it up by hand? Install ESLint plus Next.js own config package, then Prettier. Note: Next.js 16 removed the old "next lint" command — you run the ESLint CLI directly now.
$ pnpm add -D eslint eslint-config-next$ pnpm add -D --save-exact prettierNote: ESLint v9 uses "flat config" — a single eslint.config.mjs that exports an array of config objects. eslint-config-next bundles the rules that matter for Next.js: React, React Hooks, and the @next/next plugin that catches Next-specific mistakes — using a plain <img> instead of next/image, an <a> instead of <Link>, or a bad font setup. "core-web-vitals" upgrades the rules that hurt your performance score from warnings to errors; the "/typescript" config adds TypeScript checks. globalIgnores tells ESLint to skip Next.js build output.
import { defineConfig, globalIgnores } from 'eslint/config'
import nextVitals from 'eslint-config-next/core-web-vitals'
import nextTs from 'eslint-config-next/typescript'
const eslintConfig = defineConfig([
...nextVitals,
...nextTs,
// skip generated files and build output
globalIgnores(['.next/**', 'out/**', 'build/**', 'next-env.d.ts']),
])
export default eslintConfigNote: the Next.js config is sensible, but now and then you will want to silence a rule. Add your OWN object after the spread configs — ESLint applies entries in order, so later ones win — and set a rule to off, warn, or error. Two you will likely meet: the apostrophe-in-TSX rule, and the no-plain-img-tag rule.
import { defineConfig, globalIgnores } from 'eslint/config'
import nextVitals from 'eslint-config-next/core-web-vitals'
const eslintConfig = defineConfig([
...nextVitals,
{
rules: {
'react/no-unescaped-entities': 'off',
'@next/next/no-img-element': 'off',
},
},
globalIgnores(['.next/**', 'out/**', 'build/**', 'next-env.d.ts']),
])
export default eslintConfigWhy: Prettier is opinionated on purpose — there is very little to configure, which is the point: nobody argues about style. A tiny .prettierrc records the few choices your team makes so every machine formats identically. Note: on a Next.js + Tailwind project, many teams also add prettier-plugin-tailwindcss, which sorts your Tailwind class names into a consistent order.
{
"semi": false,
"singleQuote": true,
"tabWidth": 2,
"trailingComma": "es5",
"printWidth": 80
}Why: Tailwind class lists get long and everyone orders them differently, which makes diffs noisy and classes hard to scan. prettier-plugin-tailwindcss reorders them into Tailwind's recommended order every time you format — no thinking required. Note: on Tailwind v4 there is no tailwind.config.js, so you point the plugin at your CSS entry — the file with @import "tailwindcss", usually app/globals.css — using tailwindStylesheet. It must be the LAST plugin in the list. If you group classes with a helper like cn() or clsx(), add "tailwindFunctions" so those get sorted too.
$ pnpm add -D prettier-plugin-tailwindcssadd it to .prettierrc (Tailwind v4 points at the CSS file, not a config file):
{
"semi": false,
"singleQuote": true,
"tabWidth": 2,
"trailingComma": "es5",
"printWidth": 80,
"plugins": ["prettier-plugin-tailwindcss"],
"tailwindStylesheet": "./app/globals.css",
"tailwindFunctions": ["cn", "clsx"]
}format, and it rewrites class order for you — before, then after:
// before
<div className="p-4 flex text-white bg-mint-500 rounded-lg items-center">
// after
<div className="flex items-center rounded-lg bg-mint-500 p-4 text-white">Note: ESLint also has a few formatting opinions — spacing, quotes — that clash with Prettier, so they undo each other. eslint-config-prettier switches OFF every ESLint rule that overlaps with formatting, leaving ESLint to judge correctness and Prettier to own the look. With flat config you import its "/flat" entry and place it AFTER the Next.js configs so it wins.
$ pnpm add -D eslint-config-prettieradd prettier AFTER the Next.js configs in eslint.config.mjs:
import { defineConfig, globalIgnores } from 'eslint/config'
import nextVitals from 'eslint-config-next/core-web-vitals'
import prettier from 'eslint-config-prettier/flat'
const eslintConfig = defineConfig([
...nextVitals,
prettier, // <- after Next.js config: switches off the clashing rules
globalIgnores(['.next/**', 'out/**', 'build/**', 'next-env.d.ts']),
])
export default eslintConfigWhy: nobody remembers the full commands. Put them in package.json so you (and CI) just run one short command. The --fix and --write flags auto-repair everything that can be fixed safely. Note: since Next.js 16 dropped "next lint", the lint script just calls eslint directly. pnpm lets you drop the word "run" (pnpm lint); npm needs it (npm run lint).
$ pnpm lint$ pnpm lint:fix$ pnpm format$ pnpm format:check{
"scripts": {
"lint": "eslint .",
"lint:fix": "eslint . --fix",
"format": "prettier --write .",
"format:check": "prettier --check ."
}
}what each script does:
lint -> report lint problems, change nothing (use this in CI)
lint:fix -> auto-fix every lint problem ESLint can fix on its own
format -> rewrite all files with Prettier's formatting
format:check -> check formatting only, change nothing (use this in CI)Where: this is the setting that makes it effortless — install the Prettier and ESLint extensions, then drop this in .vscode/settings.json. Now every save reformats the file and applies safe lint fixes. Committing this file means teammates get the same behaviour automatically.
{
"editor.defaultFormatter": "esbenp.prettier-vscode",
"editor.formatOnSave": true,
"editor.codeActionsOnSave": {
"source.fixAll.eslint": "explicit"
}
}Why: a rule only people remember to run is not a rule. Two safety nets: a pre-commit hook (Husky + lint-staged) that checks files as you commit them, and a CI step that fails the build if anything is off. The --check / no --fix forms report problems without changing files — exactly what CI wants.
$ pnpm add -D husky lint-staged$ pnpm exec husky init$ echo "npx lint-staged" > .husky/pre-commit$ pnpm lint$ pnpm exec prettier --check .package.json — fix staged files before they are committed
{
"lint-staged": {
"*.{js,jsx,ts,tsx}": ["eslint --fix", "prettier --write"]
}
}