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 . --fixWhen 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+stylisticTypeCheckedfor the most rigorous TS ruleseslint-plugin-n(the maintained fork ofeslint-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.eslintrcfiles. typescript-eslintships as a single package that re-exports@typescript-eslint/parser,@typescript-eslint/eslint-plugin, and aconfig()helper that concatenates and types config objects.eslint-plugin-nreplaces the abandonedeslint-plugin-node. It adds Node-specific rules liken/no-missing-import,n/no-unpublished-bin, andn/no-deprecated-api.- Type-aware rules (anything suffixed
TypeChecked) require the parser to loadtsconfig.jsonviaparserOptions.project. Without it, those rules silently do nothing. tsconfigRootDir: import.meta.dirnameensures theprojectpath 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-prettierimport 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/parseris installed transitively via thetypescript-eslintmeta-package — you rarely need to import it directly.- Type-aware rules like
no-floating-promisesandno-misused-promisesare essential for Node.js scripts where silent unhandled rejections can corrupt state. - Enable the strictest preset with
...tseslint.configs.strictTypeChecked— it catches real bugs likeno-unnecessary-conditionandno-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
.eslintrcconfusion — ESLint 9+ defaults to flat config. If you still have a.eslintrc.jsonin the project, ESLint ignores it silently onceeslint.config.jsexists. Fix: Delete all legacy files when migrating and verify withnpx eslint --print-config path/to/file.ts. -
Missing
parserOptions.projectdisables type-aware rules — Rules fromstrictTypeCheckedorstylisticTypeCheckedrequire the TypeScript program to be loaded. Withoutproject, they throw at runtime or silently pass. Fix: Always setparserOptions.projectandtsconfigRootDirin the TS-files block. -
eslint-plugin-nreports false positives for TS path aliases — Rules liken/no-missing-importcan't resolve@/utilspath aliases defined intsconfig.json. Fix: Disablen/no-missing-import(andn/no-unpublished-import) when using TypeScript — typescript-eslint already validates imports. -
ESM config file in a CommonJS project — If
package.jsonhas"type": "commonjs"(or notypefield),eslint.config.jswithimportsyntax fails to load. Fix: Rename toeslint.config.mjsOR add"type": "module"topackage.json. -
Prettier/ESLint rule conflicts — Enabling stylistic ESLint rules alongside Prettier produces fighting auto-fixes. Fix: Add
eslint-config-prettieras the last item in the config array to disable conflicting rules. -
ignoresmixed with other keys silently becomes a file filter — In flat config, an object with bothignoresandrulesis 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.
| Alternative | Use When | Don't Use When |
|---|---|---|
| Biome | You want one fast Rust-based tool for lint + format | You need the full ESLint plugin ecosystem or custom rules |
| oxlint | You want extreme speed and are OK with a subset of rules | You rely on type-aware rules (not yet supported) |
| Deno lint | You're running scripts on Deno, not Node | You target Node.js |
standard | You want zero-config, opinionated defaults | You need to customize any rule |
FAQs
What replaced eslint-plugin-node?
eslint-plugin-nodeis unmaintained.eslint-plugin-nis 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-eslintmeta-package bundles the parser and plugin. - Install
typescript-eslintand usetseslint.config()— it wires both together.
How do I enable type-aware linting?
- Set
parserOptions.projectto yourtsconfig.jsonpath. - Set
parserOptions.tsconfigRootDirtoimport.meta.dirname. - Extend
...tseslint.configs.strictTypeChecked(orrecommendedTypeChecked). - 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-promisescatches forgottenawaitcalls.no-misused-promisesprevents passing async functions where sync callbacks are expected.no-unsafe-argumentcatchesanyleaking from untyped dependencies.
Gotcha: Why is my eslint.config.js not being loaded?
- Check
package.jsonfor"type". If it'scommonjsor missing, rename the config toeslint.config.mjsor add"type": "module". - ESLint 9+ requires flat config by default — legacy
.eslintrcfiles are ignored when a flat config exists. - Run
npx eslint --print-config somefile.tsto see which config is actually loaded.
Gotcha: Why does eslint-plugin-n complain about my path aliases?
n/no-missing-importcan't resolve TypeScript path aliases like@/lib/foo.- Disable
n/no-missing-importandn/no-unpublished-importin TypeScript projects. typescript-eslintalready 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-checkwith JSDoc@type \{import("eslint").Linter.Config[]\}. - ESLint 9.18+ also supports
eslint.config.tsnatively.
TypeScript: What's the difference between recommendedTypeChecked and strictTypeChecked?
recommendedTypeCheckedis the safe baseline — catches bugs without being too strict.strictTypeCheckedadds stricter rules likeno-unnecessary-conditionandprefer-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-prettieronly 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 withfiles: ["**/*.ts"]. - Apply type-aware rules only to the
.tsblock to avoid errors on plain JS files.
How do I enforce async/await over raw promise chains?
- Enable
@typescript-eslint/no-floating-promisesto catch unhandled promises. - Enable
@typescript-eslint/no-misused-promisesfor callback mismatches. - Enable
@typescript-eslint/return-awaitset to"always"for cleaner stack traces.
Related
- Utility Script Patterns — file I/O, prompts, spinners, and colored output
- ESLint Setup for Next.js — flat config for Next.js apps
- ESLint Plugins — add React, a11y, import plugins
- TypeScript Basics — tsconfig fundamentals