Chapter 42: Node.js Linting & Formatting
1. Why linting & formatting matters so much in Node.js projects
- Node.js projects tend to grow quickly and involve many developers → style chaos happens fast
- JavaScript is extremely flexible → many ways to write the same thing → teams waste time debating style
- TypeScript makes structural errors obvious, but style, readability, consistency and potential bugs are still invisible without linting
- Good linting catches subtle bugs that TypeScript misses (no-unused-vars with conditions, no-console in production, etc.)
- Automatic formatting removes 95% of style discussions → people focus on logic instead of tabs vs spaces
Two separate responsibilities (very important distinction):
| Responsibility | Tool family today (2025–2026) | Goal | Runs on save? | Blocks commit? |
|---|---|---|---|---|
| Formatting | Prettier | Consistent visual style (spaces, quotes, semicolons, line length…) | Yes | No (usually) |
| Linting | ESLint + typescript-eslint | Code quality, potential bugs, best practices, style rules that Prettier doesn’t handle | Yes | Yes (usually) |
Golden rule used by almost all good teams today:
Prettier handles how the code looks ESLint handles whether the code is correct / safe / maintainable
2. Modern recommended stack (2025–2026)
| Tool | Purpose | Current best version (early 2026) | Why it wins right now |
|---|---|---|---|
| Prettier | Code formatting | 3.3.x | De-facto standard, huge ecosystem |
| ESLint | Linting & quality rules | 9.x (flat config) | ESLint 9 flat config is finally good |
| typescript-eslint | TypeScript-specific rules | 8.x | Excellent integration with ESLint 9 |
| eslint-config-standard-with-typescript | Popular sane defaults (optional) | 43.x | Good starting point if you like StandardJS style |
| prettier-plugin-tailwindcss | Sort Tailwind classes (if using Tailwind) | latest | Essential if you use Tailwind |
| lint-staged | Run lint/format only on git staged files | 15.x | Makes pre-commit fast |
| husky | Git hooks (pre-commit, pre-push) | 9.x | Simple & reliable |
3. Step-by-step: Setting up a modern Node.js + TypeScript linting & formatting environment
Create a new project (or use existing one)
|
0 1 2 3 4 5 6 7 8 9 |
mkdir my-api cd my-api npm init -y npm pkg set type=module |
Install everything
|
0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
npm install -D \ typescript \ @types/node \ prettier \ eslint \ eslint-config-standard-with-typescript \ @typescript-eslint/parser \ @typescript-eslint/eslint-plugin \ lint-staged \ husky \ tsx \ nodemon |
Initialize TypeScript
|
0 1 2 3 4 5 6 |
npx tsc --init |
Create .prettierrc (JSON or .prettierrc.cjs)
|
0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
{ "semi": false, "trailingComma": "es5", "singleQuote": true, "printWidth": 100, "tabWidth": 2, "useTabs": false, "bracketSpacing": true, "arrowParens": "always", "plugins": ["prettier-plugin-tailwindcss"] } |
Create .prettierignore
|
0 1 2 3 4 5 6 7 8 9 10 11 12 |
dist node_modules coverage *.min.js package-lock.json pnpm-lock.yaml bun.lockb |
Create eslint.config.mjs (ESLint 9+ flat config – the modern way)
|
0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 |
import js from '@eslint/js' import ts from 'typescript-eslint' import standardWithTs from 'eslint-config-standard-with-typescript' export default [ js.configs.recommended, ...ts.configs.recommended, ...ts.configs.stylistic, // You can use standard-with-typescript as base (optional) ...standardWithTs, { files: ['**/*.ts', '**/*.tsx'], languageOptions: { parserOptions: { project: './tsconfig.json', tsconfigRootDir: import.meta.dirname, }, }, rules: { // Turn off some annoying / opinionated rules '@typescript-eslint/explicit-function-return-type': 'off', '@typescript-eslint/no-floating-promises': 'error', '@typescript-eslint/no-misused-promises': 'error', '@typescript-eslint/consistent-type-imports': 'error', '@typescript-eslint/no-unused-vars': ['error', { argsIgnorePattern: '^_' }], 'no-console': ['warn', { allow: ['warn', 'error'] }], 'no-restricted-syntax': [ 'error', { selector: 'TSEnumDeclaration', message: 'Use const enum or object literal instead of enum' } ] } }, { ignores: ['dist/**', 'node_modules/**', '*.js'] } ] |
Create .eslintrc.ignore (optional)
|
0 1 2 3 4 5 6 7 8 9 |
dist node_modules coverage *.min.js |
Add lint-staged + husky (run on git commit)
|
0 1 2 3 4 5 6 7 8 9 10 11 12 |
// package.json { "lint-staged": { "*.{js,ts,tsx}": "eslint --fix", "*.{js,ts,tsx,json,md,css}": "prettier --write" } } |
|
0 1 2 3 4 5 6 7 8 |
# One-time setup npx husky init echo "npx lint-staged" > .husky/pre-commit |
Now every commit will:
- Run ESLint + auto-fix on staged .ts / .js files
- Run Prettier on staged files
4. Realistic example files – what good code looks like
src/index.ts
|
0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 |
import { randomUUID } from 'node:crypto' interface User { id: string name: string email: string createdAt: Date } function createUser(name: string, email: string): User { if (!email.includes('@')) { throw new Error('Invalid email') } return { id: randomUUID(), name, email, createdAt: new Date(), } } const user = createUser('Aman', 'aman@example.com') console.log('User created:', user) |
src/utils/logger.ts
|
0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
import { format } from 'node:util' type LogLevel = 'info' | 'warn' | 'error' export function log(level: LogLevel, message: string, ...args: unknown[]): void { const timestamp = new Date().toISOString() const coloredLevel = { info: `\x1b[32m${level.toUpperCase()}\x1b[0m`, warn: `\x1b[33m${level.toUpperCase()}\x1b[0m`, error: `\x1b[31m${level.toUpperCase()}\x1b[0m`, }[level] console.log(`[${timestamp}] {coloredLevel}: ${format(message, ...args)}`) } |
Run linters manually:
|
0 1 2 3 4 5 6 7 |
npm run lint npm run format |
5. Summary – Modern linting & formatting checklist (2025–2026)
| Task / Goal | Tool / Setting | Typical command / config |
|---|---|---|
| Format on save | Prettier + VS Code extension | Editor: “editor.formatOnSave”: true |
| Lint on save | ESLint + VS Code extension | Editor: “eslint.format.enable”: true |
| Auto-fix on commit | lint-staged + husky | pre-commit hook: npx lint-staged |
| Use flat config (ESLint 9+) | eslint.config.mjs / .js | Recommended for new projects |
| Strict TypeScript rules | @typescript-eslint/recommended + stylistic | Catches many subtle bugs |
| No unused variables | ‘@typescript-eslint/no-unused-vars’ | Must be ‘error’ in serious projects |
| Prefer const over let when possible | ‘prefer-const’ | Enabled by default in most configs |
| No console.log in production code | ‘no-console’ with allow: [‘warn’, ‘error’] | Very common rule |
Which topic would you like to go much deeper into next?
- Full ESLint flat config with 30–40 real rules (strict mode)
- Prettier vs ESLint style rules conflict resolution
- lint-staged + husky setup with pre-push checks
- Tailwind + prettier-plugin-tailwindcss perfect setup
- TypeScript + ESLint best rules for large Node.js backends
Just tell me what you want to focus on — I’ll go as deep as you like with complete files and explanations. 😊
