Adding ESLint to a Nuxt app for the first time is rarely just "install and go" once the codebase is real and has actual history. Here's exactly what happened wiring it into Tabeer.ai's Nuxt 4 frontend, including the parts where the default rule set was simply wrong for this specific codebase's conventions.
Setup: the Official Module, Not a Hand-Rolled Config
npm install -D @nuxt/eslint eslint
// nuxt.config.ts
modules: [/* ...existing modules... */, '@nuxt/eslint'],
@nuxt/eslint generates a base flat config matching Nuxt's own conventions
(npx nuxt prepare writes it to .nuxt/eslint.config.mjs), extended from the project root:
// eslint.config.mjs
import withNuxt from './.nuxt/eslint.config.mjs'
export default withNuxt({
rules: {
'vue/multi-word-component-names': 'off',
'vue/no-v-html': 'warn',
'@typescript-eslint/no-explicit-any': 'warn',
},
})
The First Run: 298 Problems
npx eslint .
233 errors, 65 warnings. The overwhelming majority — 226 of them, after auto-fixing what was
mechanical — were @typescript-eslint/no-explicit-any.
The Call That Actually Matters: Don't Error on any Here
The default TypeScript-ESLint recommendation errors on any. That's correct guidance in
general and the wrong specific setting for this codebase: any is used deliberately across
roughly 230 call sites for loosely-typed API response data — a common, accepted pattern in
Nuxt/Vue codebases that don't maintain a hand-written type layer over every backend endpoint's
JSON shape.
Setting the rule to error here wouldn't have caught new problems — it would have made every
single lint run fail from day one, with 230+ pre-existing violations nobody was going to fix
in one pass. That's not a functioning lint gate, it's a red CI check everyone learns to ignore.
Downgraded to warn instead: visible, trackable, doesn't block anything, and any new any
usage still shows up for review without needing to fix two hundred existing ones first.
The actual principle: a linter's default rule set is written for an idealized codebase that doesn't exist yet. Match the config to the codebase you actually have, not the one a style guide assumes you have — a rule nobody can realistically satisfy stops functioning as a rule.
The 65 Mechanical Fixes and 7 Real Bugs
npx eslint . --fix
65 warnings auto-fixed (mostly vue/html-self-closing — <img>/<input> tags missing their
self-closing slash). That left 7 genuine errors, each worth a specific look:
Real dead code the linter correctly caught: a compact prop declared via defineProps,
accepted by one parent component (<HomeDailyChallengeSection compact />), and never actually
read anywhere in the child. Not a lint false positive — an incomplete feature, flagged
accurately. Fixed by prefixing the unused binding (_props) with a comment explaining what's
actually going on, rather than guessing at implementing the missing behavior mid-lint-cleanup.
Ternaries used purely for side effects, twice:
next.has(id) ? next.delete(id) : next.add(id) // flagged: expression value is discarded
Rewritten as plain if/else — same runtime behavior, no ambiguity about intent, satisfies
the linter because it's genuinely clearer code, not just quieter code.
A legitimate IIFE pattern that looks like a bug to a generic rule: the PostHog tracking
snippet includes e.__SV || (...) — a short-circuit-OR used as a guard, straight from
PostHog's own official snippet. no-unused-expressions correctly flags this as an odd-looking
bare expression; it's also exactly correct as written, and not something to "simplify" —
this exact snippet had a real, shipped bug earlier from a well-intentioned rewrite. Handled
with a scoped eslint-disable/eslint-enable block and a comment pointing future editors at
why:
// Faithful port of the official PostHog snippet's short-circuit init guard — do not
// "simplify" this, see the snippet-transcription bug this exact pattern caused earlier
// in this project's history.
/* eslint-disable @typescript-eslint/no-unused-expressions */
e.__SV || (...)
/* eslint-enable @typescript-eslint/no-unused-expressions */
Three more unused-variable cases — a config never read after being declared, an unused
auth store instance, a bare catch (e) where e was never used (simplified to catch {})
— each a small, real cleanup, each confirmed against the surrounding code before removing
anything, not assumed dead from the lint output alone.
What Verifying Actually Looked Like
Every fix, then a full production build:
npm run build
Then Ruff's counterpart lesson applied here too: don't trust that a lint-driven change is safe just because it compiles — actually re-run the app's tests after.
The Actual Takeaway
A linter's value on a first run isn't "zero problems, ship it" — it's "which of these 298 things are real, and which are noise from a default config that doesn't match this codebase." Getting that split right (233 warnings that stay warnings, 7 errors that get fixed by hand, one pattern that gets an explicit, documented exception) is the actual work; blindly enabling every recommended rule and letting a mass-fix tool loose on the result is how you end up with either a broken build or a build in disguise as a broken build no one trusts.
If you're rolling out ESLint on a Vue or Nuxt codebase that's grown without one and want the config tuned to what the codebase actually does rather than a generic preset, get in touch.
By Shahid Malik