All Articles
Code Quality

ESLint on a Real Nuxt App: 298 Findings, and Why I Didn't Error on 233 of Them

Shahid MalikBy Shahid MalikSeptember 4, 20267 min read

Adding @nuxt/eslint to Tabeer.ai's frontend for the first time surfaced 298 problems — most of them `any`. Here's the actual config, why erroring on all of it would have been the wrong call, and the real bugs the linter caught underneath the noise.

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.

Related Articles

Testing

Setting Up Vitest on a Nuxt App With Zero Existing Tests

Tabeer.ai's frontend had no unit tests at all before this. Rather than write a sprawling first test suite, here's the actual config and the one real test that proves the setup works — a foundation to build on, not a coverage number to chase.

5 min read
Shahid Malik - AI-First Odoo Consultant

Shahid Malik

AI-First Odoo ERP Specialist

Shahid Malik is an AI-first Odoo consultant helping businesses solve complex ERP and business process challenges. His work combines Odoo consulting, process optimization, automation, integrations, migrations, and practical AI solutions to build scalable and reliable business systems.

Book a consultation for your Odoo project
Discuss Your Odoo Project