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.
Search across all documentation pages
Configure ESLint flat config (eslint.config.js) for Node.js script projects with TypeScript support, Node-specific rules, and type-aware linting.
🤖 Read the SystemsArchitect.io Blog for over 100+ cloud architecture articles 🔥
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.
// 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:
tseslint.config() helper for type-safe config authoringstrictTypeChecked + stylisticTypeChecked for the most rigorous TS ruleseslint-plugin-n (the maintained fork of eslint-plugin-node) providing Node.js-specific rulesparserOptions.projecteslint.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.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.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-eslint/parser is installed transitively via the typescript-eslint meta-package - you rarely need to import it directly.no-floating-promises and no-misused-promises are essential for Node.js scripts where silent unhandled rejections can corrupt state....tseslint.configs.strictTypeChecked - it catches real bugs like no-unnecessary-condition and no-unsafe-argument.tseslint.config() helper provides autocomplete and type errors if you misspell a rule name or option.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.
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 |
eslint-plugin-node is unmaintained.eslint-plugin-n is the community-maintained fork with flat config support.n/ prefix (e.g. n/no-missing-import).typescript-eslint meta-package bundles the parser and plugin.typescript-eslint and use tseslint.config() - it wires both together.parserOptions.project to your tsconfig.json path.parserOptions.tsconfigRootDir to import.meta.dirname....tseslint.configs.strictTypeChecked (or recommendedTypeChecked).project, type-aware rules either error or silently pass.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.package.json for "type". If it's commonjs or missing, rename the config to eslint.config.mjs or add "type": "module"..eslintrc files are ignored when a flat config exists.npx eslint --print-config somefile.ts to see which config is actually loaded.n/no-missing-import can't resolve TypeScript path aliases like @/lib/foo.n/no-missing-import and n/no-unpublished-import in TypeScript projects.typescript-eslint already verifies imports via the TypeScript compiler.tseslint.config() helper - it provides full autocomplete and type errors.// @ts-check with JSDoc @type \{import("eslint").Linter.Config[]\}.eslint.config.ts natively.recommendedTypeChecked is the safe baseline - catches bugs without being too strict.strictTypeChecked adds stricter rules like no-unnecessary-condition and prefer-reduce-type-parameter.files: ["scripts/**/*.ts"] and override rules inside it.eslint-config-prettier only disables stylistic rules that conflict with Prettier.files: ["**/*.js"] and one with files: ["**/*.ts"]..ts block to avoid errors on plain JS files.@typescript-eslint/no-floating-promises to catch unhandled promises.@typescript-eslint/no-misused-promises for callback mismatches.@typescript-eslint/return-await set to "always" for cleaner stack traces.Reviewed by Chris St. John·Last updated Jul 10, 2026
🤖 Read the SystemsArchitect.io Blog for over 100+ cloud architecture articles 🔥