React SME Cookbook
All FAQs

Search Documentation

Search across all documentation pages

eslintnodejsscriptstypescriptlinting

ESLint for Node.js Scripts

Configure ESLint flat config (eslint.config.js) for Node.js script projects with TypeScript support, Node-specific rules, and type-aware linting.

Recipe

Quick-reference recipe card — copy-paste ready.

# Initialize a new ESLint config interactively
npm init @eslint/config@latest
 
# Install the core toolchain for a TypeScript Node.js script project
npm install --save-dev \
  eslint \
  typescript-eslint \
  eslint-plugin-n \
  globals
 
# Lint your scripts
npx eslint .
 
# Lint and auto-fix
npx eslint . --fix

When to reach for this: Any standalone Node.js project — CLIs, build scripts, automation, or monorepo tooling packages — that lives outside a Next.js/React app.

Working Example

// eslint.config.js
import js from "@eslint/js";
import tseslint from "typescript-eslint";
import nodePlugin from "eslint-plugin-n";
import globals from "globals";
 
export default tseslint.config(
  // Global ignores — must be the sole key in its own object
  {
    ignores: ["dist/", "build/", "coverage/", "node_modules/"],
  },
 
  // Base JS rules
  js.configs.recommended,
 
  // Node.js plugin recommended rules (flat config preset)
  nodePlugin.configs["flat/recommended-script"],
 
  // TypeScript type-checked rules
  ...tseslint.configs.strictTypeChecked,
  ...tseslint.configs.stylisticTypeChecked,
 
  {
    files: ["**/*.ts", "**/*.mts"],
    languageOptions: {
      globals: globals.nodeBuiltin,
      parserOptions: {
        project: "./tsconfig.json",
        tsconfigRootDir: import.meta.dirname,
      },
    },
    rules: {
      // Enforce async/await over raw promise chains
      "promise/prefer-await-to-then": "off",
      "@typescript-eslint/no-floating-promises": "error",
      "@typescript-eslint/no-misused-promises": "error",
      "@typescript-eslint/return-await": ["error", "always"],
 
      // Node.js plugin rules
      "n/no-missing-import": "off", // typescript-eslint resolves imports
      "n/no-unpublished-import": "off",
      "n/no-process-exit": "warn",
    },
  },
);

What this demonstrates:

  • Flat config with the tseslint.config() helper for type-safe config authoring
  • strictTypeChecked + stylisticTypeChecked for the most rigorous TS rules
  • eslint-plugin-n (the maintained fork of eslint-plugin-node) providing Node.js-specific rules
  • Type-aware linting enabled via parserOptions.project
  • Global ignores in a dedicated object (required by flat config)

Deep Dive

How It Works

  • ESLint 9+ loads eslint.config.js (flat config) from the project root by default — no more cascading .eslintrc files.
  • typescript-eslint ships as a single package that re-exports @typescript-eslint/parser, @typescript-eslint/eslint-plugin, and a config() helper that concatenates and types config objects.
  • eslint-plugin-n replaces the abandoned eslint-plugin-node. It adds Node-specific rules like n/no-missing-import, n/no-unpublished-bin, and n/no-deprecated-api.
  • Type-aware rules (anything suffixed TypeChecked) require the parser to load tsconfig.json via parserOptions.project. Without it, those rules silently do nothing.
  • tsconfigRootDir: import.meta.dirname ensures the project path resolves relative to the config file, not the current working directory.

Variations

JavaScript-only config (no TypeScript):

// eslint.config.js
import js from "@eslint/js";
import nodePlugin from "eslint-plugin-n";
import globals from "globals";
 
export default [
  js.configs.recommended,
  nodePlugin.configs["flat/recommended-script"],
  {
    languageOptions: {
      ecmaVersion: "latest",
      sourceType: "module",
      globals: globals.nodeBuiltin,
    },
  },
];

Type-aware linting with a dedicated tsconfig:

{
  files: ["**/*.ts"],
  languageOptions: {
    parserOptions: {
      project: "./tsconfig.eslint.json",
      tsconfigRootDir: import.meta.dirname,
    },
  },
}

Prettier integration (turn off stylistic rules that conflict):

npm install --save-dev eslint-config-prettier
import prettier from "eslint-config-prettier";
 
export default tseslint.config(
  js.configs.recommended,
  ...tseslint.configs.recommended,
  prettier, // must be LAST — disables conflicting stylistic rules
);

Scoped ignores for generated files:

{
  ignores: ["**/*.generated.ts", "src/proto/**"],
}

TypeScript Notes

  • @typescript-eslint/parser is installed transitively via the typescript-eslint meta-package — you rarely need to import it directly.
  • Type-aware rules like no-floating-promises and no-misused-promises are essential for Node.js scripts where silent unhandled rejections can corrupt state.
  • Enable the strictest preset with ...tseslint.configs.strictTypeChecked — it catches real bugs like no-unnecessary-condition and no-unsafe-argument.
  • The tseslint.config() helper provides autocomplete and type errors if you misspell a rule name or option.

Gotchas

Things that will bite you. Each gotcha includes what goes wrong, why it happens, and the fix.

  • Flat config vs legacy .eslintrc confusion — ESLint 9+ defaults to flat config. If you still have a .eslintrc.json in the project, ESLint ignores it silently once eslint.config.js exists. Fix: Delete all legacy files when migrating and verify with npx eslint --print-config path/to/file.ts.

  • Missing parserOptions.project disables type-aware rules — Rules from strictTypeChecked or stylisticTypeChecked require the TypeScript program to be loaded. Without project, they throw at runtime or silently pass. Fix: Always set parserOptions.project and tsconfigRootDir in the TS-files block.

  • eslint-plugin-n reports false positives for TS path aliases — Rules like n/no-missing-import can't resolve @/utils path aliases defined in tsconfig.json. Fix: Disable n/no-missing-import (and n/no-unpublished-import) when using TypeScript — typescript-eslint already validates imports.

  • ESM config file in a CommonJS project — If package.json has "type": "commonjs" (or no type field), eslint.config.js with import syntax fails to load. Fix: Rename to eslint.config.mjs OR add "type": "module" to package.json.

  • Prettier/ESLint rule conflicts — Enabling stylistic ESLint rules alongside Prettier produces fighting auto-fixes. Fix: Add eslint-config-prettier as the last item in the config array to disable conflicting rules.

  • ignores mixed with other keys silently becomes a file filter — In flat config, an object with both ignores and rules is treated as a filter, not a global ignore. Fix: Put global ignores in their own { ignores: [...] } object with no other keys.

Alternatives

Other ways to solve the same problem — and when each is the better choice.

AlternativeUse WhenDon't Use When
BiomeYou want one fast Rust-based tool for lint + formatYou need the full ESLint plugin ecosystem or custom rules
oxlintYou want extreme speed and are OK with a subset of rulesYou rely on type-aware rules (not yet supported)
Deno lintYou're running scripts on Deno, not NodeYou target Node.js
standardYou want zero-config, opinionated defaultsYou need to customize any rule

FAQs

What replaced eslint-plugin-node?
  • eslint-plugin-node is unmaintained.
  • eslint-plugin-n is the community-maintained fork with flat config support.
  • It provides the same rules under the n/ prefix (e.g. n/no-missing-import).
Do I need @typescript-eslint/parser as a separate install?
  • No. The typescript-eslint meta-package bundles the parser and plugin.
  • Install typescript-eslint and use tseslint.config() — it wires both together.
How do I enable type-aware linting?
  • Set parserOptions.project to your tsconfig.json path.
  • Set parserOptions.tsconfigRootDir to import.meta.dirname.
  • Extend ...tseslint.configs.strictTypeChecked (or recommendedTypeChecked).
  • Without project, type-aware rules either error or silently pass.
Why are strictTypeChecked rules better for Node.js scripts?
  • Scripts often deal with file I/O and child processes where silent failures corrupt data.
  • no-floating-promises catches forgotten await calls.
  • no-misused-promises prevents passing async functions where sync callbacks are expected.
  • no-unsafe-argument catches any leaking from untyped dependencies.
Gotcha: Why is my eslint.config.js not being loaded?
  • Check package.json for "type". If it's commonjs or missing, rename the config to eslint.config.mjs or add "type": "module".
  • ESLint 9+ requires flat config by default — legacy .eslintrc files are ignored when a flat config exists.
  • Run npx eslint --print-config somefile.ts to see which config is actually loaded.
Gotcha: Why does eslint-plugin-n complain about my path aliases?
  • n/no-missing-import can't resolve TypeScript path aliases like @/lib/foo.
  • Disable n/no-missing-import and n/no-unpublished-import in TypeScript projects.
  • typescript-eslint already verifies imports via the TypeScript compiler.
TypeScript: How do I type my eslint.config.js file?
  • Use the tseslint.config() helper — it provides full autocomplete and type errors.
  • Alternatively, add // @ts-check with JSDoc @type \{import("eslint").Linter.Config[]\}.
  • ESLint 9.18+ also supports eslint.config.ts natively.
TypeScript: What's the difference between recommendedTypeChecked and strictTypeChecked?
  • recommendedTypeChecked is the safe baseline — catches bugs without being too strict.
  • strictTypeChecked adds stricter rules like no-unnecessary-condition and prefer-reduce-type-parameter.
  • For new projects, start strict. For migrations, start recommended and tighten later.
How do I disable rules only for a specific directory?
  • Add a new config object with files: ["scripts/**/*.ts"] and override rules inside it.
  • Flat config is order-sensitive: later objects override earlier ones for matching files.
Why put Prettier last in the config array?
  • eslint-config-prettier only disables stylistic rules that conflict with Prettier.
  • It must run after any preset that enables those rules, otherwise the presets re-enable them.
  • Always place it as the last item in the array.
Can I run ESLint on JavaScript and TypeScript files in the same config?
  • Yes. Use two config objects: one with files: ["**/*.js"] and one with files: ["**/*.ts"].
  • Apply type-aware rules only to the .ts block to avoid errors on plain JS files.
How do I enforce async/await over raw promise chains?
  • Enable @typescript-eslint/no-floating-promises to catch unhandled promises.
  • Enable @typescript-eslint/no-misused-promises for callback mismatches.
  • Enable @typescript-eslint/return-await set to "always" for cleaner stack traces.