Episode 3 — NodeJS MongoDB Backend Architecture / 3.13 — Production Project Structure

3.13.e --- ESLint and Prettier

In one sentence: ESLint catches bugs and enforces code quality rules, Prettier enforces consistent formatting --- together they eliminate style arguments and catch errors before code reaches production.

Navigation: <- 3.13.d Production Environment | 3.13.f --- Testing APIs with Postman ->


1. ESLint: what it does

ESLint is a static analysis tool for JavaScript. It reads your code without executing it and flags:

CategoryExamples
Possible bugsUsing == instead of ===, unreachable code after return
Code qualityUnused variables, missing return in array callbacks
Best practicesvar instead of let/const, eval() usage
Style conventionsNaming patterns, import order (though Prettier handles formatting)

ESLint is configurable --- you choose which rules to enable, disable, or set as warnings vs errors.


2. Installing and configuring ESLint

Installation

npm install -D eslint

Interactive setup

npx eslint --init

This wizard asks:

  1. How would you like to use ESLint? -> To check syntax and find problems
  2. What type of modules? -> CommonJS (for Node.js with require)
  3. Which framework? -> None (backend project)
  4. Does your project use TypeScript? -> No
  5. Where does your code run? -> Node
  6. What format for config? -> JavaScript

Manual .eslintrc.js configuration

// .eslintrc.js
module.exports = {
  env: {
    node: true,                 // Node.js global variables (process, __dirname)
    es2021: true,               // Modern JS syntax support
    jest: true,                 // Jest globals (describe, it, expect)
  },
  extends: [
    'eslint:recommended',       // ESLint's built-in recommended rules
  ],
  parserOptions: {
    ecmaVersion: 'latest',      // Latest ECMAScript syntax
    sourceType: 'commonjs',     // CommonJS modules (require/module.exports)
  },
  rules: {
    // --- Possible errors ---
    'no-console': 'warn',              // Warn on console.log (use logger in production)
    'no-unused-vars': ['error', {
      argsIgnorePattern: '^_',         // Allow _req, _next (intentionally unused params)
      varsIgnorePattern: '^_',
    }],
    'no-undef': 'error',               // Catch undefined variables

    // --- Best practices ---
    'prefer-const': 'error',           // Use const when variable is never reassigned
    'no-var': 'error',                 // Never use var
    'eqeqeq': ['error', 'always'],    // Always use === and !==
    'no-return-await': 'error',        // Unnecessary return await
    'no-throw-literal': 'error',       // Only throw Error objects
    'require-await': 'error',          // async functions must contain await
    'no-param-reassign': 'warn',       // Avoid mutating function parameters

    // --- Node.js specific ---
    'no-process-exit': 'warn',         // Prefer graceful shutdown
    'handle-callback-err': 'error',    // Handle error parameter in callbacks
    'no-path-concat': 'error',         // Use path.join() instead of string concat
  },
};

Popular presets

PresetWhat it includesWhen to use
eslint:recommendedCore ESLint rulesMinimum baseline for any project
airbnb-baseAirbnb's style guide (no React)Opinionated, strict, well-documented
standardStandardJS rules (no semicolons)If you prefer no-semicolon style
plugin:node/recommendedNode.js-specific rulesBackend projects

Using airbnb-base

npm install -D eslint-config-airbnb-base eslint-plugin-import
// .eslintrc.js
module.exports = {
  extends: ['airbnb-base'],
  env: { node: true, jest: true },
  rules: {
    'no-console': 'warn',
    'no-unused-vars': ['error', { argsIgnorePattern: '^_' }],
  },
};

3. Important ESLint rules for Node.js

RuleLevelWhy
no-unused-varserrorDead code = confusion. The _ pattern allows intentional unused params like Express middleware (req, res, next)
no-consolewarnIn production, use a structured logger (Winston, Pino). Warn during dev, error in CI
prefer-consterrorSignals immutability. If a let is never reassigned, it should be const
eqeqeqerror== has type coercion quirks (0 == "" is true). Always use ===
no-return-awaiterrorreturn await promise is redundant --- return promise does the same thing
no-throw-literalerrorthrow "error" loses the stack trace. Always throw new Error("...")
require-awaiterrorAn async function without await is misleading --- it wraps the return in a Promise unnecessarily
no-path-concaterror__dirname + '/file.js' breaks on Windows. Use path.join(__dirname, 'file.js')

4. Prettier: what it does

Prettier is an opinionated code formatter. It does not check for bugs --- it only enforces consistent style:

What Prettier handlesWhat Prettier does NOT handle
Indentation (tabs/spaces)Unused variables
Quote style (single/double)Missing error handling
Semicolons (with/without)Code complexity
Line length and wrappingNaming conventions
Trailing commasLogic errors
Bracket spacingPerformance issues

Key insight: ESLint catches bugs. Prettier formats code. They complement each other.


5. Installing and configuring Prettier

Installation

npm install -D prettier

.prettierrc configuration

{
  "singleQuote": true,
  "tabWidth": 2,
  "trailingComma": "all",
  "semi": true,
  "printWidth": 100,
  "bracketSpacing": true,
  "arrowParens": "always",
  "endOfLine": "lf"
}

Configuration options explained

OptionValueWhat it does
singleQuotetrue'hello' instead of "hello"
tabWidth22-space indentation
trailingComma"all"Trailing commas everywhere legal (cleaner diffs)
semitrueAdd semicolons at statement ends
printWidth100Wrap lines longer than 100 characters
bracketSpacingtrue{ foo: bar } instead of {foo: bar}
arrowParens"always"(x) => x instead of x => x
endOfLine"lf"Unix line endings (prevents cross-OS issues)

Before and after Prettier

// BEFORE: inconsistent style
const  getUser=async(req,res)=>{
  const user= await User.findById(req.params.id )
  if(!user){
    throw new ApiError(404,"User not found")
  }
      res.json(  new ApiResponse(200,user,"User fetched"))
}

// AFTER: Prettier formats it
const getUser = async (req, res) => {
  const user = await User.findById(req.params.id);
  if (!user) {
    throw new ApiError(404, 'User not found');
  }
  res.json(new ApiResponse(200, user, 'User fetched'));
};

6. Combining ESLint + Prettier

ESLint has some formatting rules that conflict with Prettier. You need two packages to resolve this:

npm install -D eslint-config-prettier eslint-plugin-prettier
PackagePurpose
eslint-config-prettierDisables all ESLint rules that conflict with Prettier
eslint-plugin-prettierRuns Prettier as an ESLint rule (reports formatting issues as ESLint errors)

Updated .eslintrc.js

// .eslintrc.js
module.exports = {
  env: {
    node: true,
    es2021: true,
    jest: true,
  },
  extends: [
    'eslint:recommended',
    'plugin:prettier/recommended',  // MUST be last --- disables conflicting rules
  ],
  parserOptions: {
    ecmaVersion: 'latest',
    sourceType: 'commonjs',
  },
  rules: {
    'no-console': 'warn',
    'no-unused-vars': ['error', { argsIgnorePattern: '^_' }],
    'prefer-const': 'error',
    'no-var': 'error',
    'eqeqeq': ['error', 'always'],
    'no-return-await': 'error',
    'no-throw-literal': 'error',

    // Prettier integration
    'prettier/prettier': ['error', {}, { usePrettierrc: true }],
  },
};

plugin:prettier/recommended must be the last entry in extends --- it disables conflicting ESLint formatting rules and enables the Prettier plugin.


7. VS Code integration

Required extensions

ExtensionPublisherPurpose
ESLintMicrosoftShows ESLint errors inline
PrettierPrettierFormats on save

VS Code settings (.vscode/settings.json)

{
  "editor.defaultFormatter": "esbenp.prettier-vscode",
  "editor.formatOnSave": true,
  "editor.codeActionsOnSave": {
    "source.fixAll.eslint": "explicit"
  },
  "eslint.validate": ["javascript", "typescript"],
  "files.eol": "\n"
}

How it works together

  1. You type code with issues
  2. ESLint shows red/yellow squiggly lines for bugs and quality issues
  3. You save the file
  4. Prettier auto-formats the code (indentation, quotes, semicolons)
  5. ESLint auto-fix runs and fixes simple lint issues (unused imports, var -> const)
  6. Remaining ESLint errors (actual bugs) stay highlighted for you to fix manually

8. npm scripts for linting and formatting

{
  "scripts": {
    "lint": "eslint .",
    "lint:fix": "eslint . --fix",
    "format": "prettier --write .",
    "format:check": "prettier --check ."
  }
}
ScriptWhen to useWhat it does
npm run lintCI pipeline, manual checkReports all ESLint errors (exit code 1 if errors found)
npm run lint:fixDevelopmentAuto-fixes what it can, reports the rest
npm run formatDevelopmentReformats all files in place
npm run format:checkCI pipelineChecks formatting without modifying (exit code 1 if unformatted)

9. .eslintignore and .prettierignore

Tell ESLint and Prettier to skip files that should not be linted or formatted:

.eslintignore

node_modules/
dist/
build/
coverage/
logs/
public/
*.min.js

.prettierignore

node_modules/
dist/
build/
coverage/
logs/
public/
package-lock.json
*.min.js
*.md

Why ignore package-lock.json? Prettier would reformat the generated file, creating huge diffs with no value.

Why ignore *.md? Optional --- some teams format markdown, others prefer manual control.


10. lint-staged + Husky: auto-lint on commit

Instead of linting the entire codebase on every commit (slow), lint-staged runs linters only on files that are staged (git added).

Installation

npm install -D lint-staged

Configuration in package.json

{
  "lint-staged": {
    "*.js": [
      "eslint --fix",
      "prettier --write"
    ],
    "*.json": [
      "prettier --write"
    ],
    "*.md": [
      "prettier --write"
    ]
  }
}

Husky pre-commit hook

# .husky/pre-commit
npx lint-staged

What happens on git commit

1. Developer runs: git commit -m "feat(user): add avatar upload"
2. Husky triggers the pre-commit hook
3. lint-staged identifies which .js files are staged
4. ESLint --fix runs on those files only
5. Prettier --write runs on those files only
6. If ESLint finds unfixable errors --> commit is BLOCKED
7. If everything passes --> commit proceeds

The full toolchain together

Developer writes code
    |
    v
VS Code ESLint extension (inline feedback)
    |
    v
Developer saves file
    |
    v
Prettier formats on save (VS Code)
    |
    v
Developer stages and commits
    |
    v
Husky pre-commit hook
    |
    v
lint-staged runs ESLint + Prettier on staged files
    |
    v
commitlint validates commit message format
    |
    v
Commit succeeds or is blocked

11. Complete setup summary

Files created

project-root/
├── .eslintrc.js         --- ESLint configuration
├── .eslintignore        --- Files ESLint should skip
├── .prettierrc          --- Prettier configuration
├── .prettierignore      --- Files Prettier should skip
├── .husky/
│   ├── pre-commit       --- Runs lint-staged
│   ├── pre-push         --- Runs test suite
│   └── commit-msg       --- Runs commitlint
├── commitlint.config.js --- Commit message rules
└── package.json         --- Scripts + lint-staged config

Packages installed

# Linting
npm install -D eslint eslint-config-prettier eslint-plugin-prettier

# Formatting
npm install -D prettier

# Git hooks
npm install -D husky lint-staged

# Commit messages (optional but recommended)
npm install -D @commitlint/cli @commitlint/config-conventional

12. Key takeaways

  1. ESLint finds bugs (unused vars, missing awaits, type coercion). Prettier formats code (indentation, quotes, semicolons). They solve different problems.
  2. Use eslint-config-prettier + eslint-plugin-prettier to combine them without conflicts.
  3. VS Code format-on-save gives instant feedback. lint-staged + Husky catches anything that slips through.
  4. plugin:prettier/recommended must be the last entry in ESLint's extends array.
  5. lint-staged only checks staged files --- fast commits even in large codebases.
  6. The full pipeline: write -> save (Prettier) -> commit (lint-staged + commitlint) -> push (tests).

Explain-It Challenge

Explain without notes:

  1. What is the difference between what ESLint catches and what Prettier fixes? Give two examples of each.
  2. Why does eslint-config-prettier need to be the last item in the extends array?
  3. A teammate commits code with a console.log left in. Describe the chain of tools that should have caught this and at which stage.

Navigation: <- 3.13.d Production Environment | 3.13.f --- Testing APIs with Postman ->