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:
| Category | Examples |
|---|---|
| Possible bugs | Using == instead of ===, unreachable code after return |
| Code quality | Unused variables, missing return in array callbacks |
| Best practices | var instead of let/const, eval() usage |
| Style conventions | Naming 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:
- How would you like to use ESLint? -> To check syntax and find problems
- What type of modules? -> CommonJS (for Node.js with
require) - Which framework? -> None (backend project)
- Does your project use TypeScript? -> No
- Where does your code run? -> Node
- 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
| Preset | What it includes | When to use |
|---|---|---|
eslint:recommended | Core ESLint rules | Minimum baseline for any project |
airbnb-base | Airbnb's style guide (no React) | Opinionated, strict, well-documented |
standard | StandardJS rules (no semicolons) | If you prefer no-semicolon style |
plugin:node/recommended | Node.js-specific rules | Backend 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
| Rule | Level | Why |
|---|---|---|
no-unused-vars | error | Dead code = confusion. The _ pattern allows intentional unused params like Express middleware (req, res, next) |
no-console | warn | In production, use a structured logger (Winston, Pino). Warn during dev, error in CI |
prefer-const | error | Signals immutability. If a let is never reassigned, it should be const |
eqeqeq | error | == has type coercion quirks (0 == "" is true). Always use === |
no-return-await | error | return await promise is redundant --- return promise does the same thing |
no-throw-literal | error | throw "error" loses the stack trace. Always throw new Error("...") |
require-await | error | An async function without await is misleading --- it wraps the return in a Promise unnecessarily |
no-path-concat | error | __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 handles | What Prettier does NOT handle |
|---|---|
| Indentation (tabs/spaces) | Unused variables |
| Quote style (single/double) | Missing error handling |
| Semicolons (with/without) | Code complexity |
| Line length and wrapping | Naming conventions |
| Trailing commas | Logic errors |
| Bracket spacing | Performance 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
| Option | Value | What it does |
|---|---|---|
singleQuote | true | 'hello' instead of "hello" |
tabWidth | 2 | 2-space indentation |
trailingComma | "all" | Trailing commas everywhere legal (cleaner diffs) |
semi | true | Add semicolons at statement ends |
printWidth | 100 | Wrap lines longer than 100 characters |
bracketSpacing | true | { 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
| Package | Purpose |
|---|---|
eslint-config-prettier | Disables all ESLint rules that conflict with Prettier |
eslint-plugin-prettier | Runs 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
| Extension | Publisher | Purpose |
|---|---|---|
| ESLint | Microsoft | Shows ESLint errors inline |
| Prettier | Prettier | Formats 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
- You type code with issues
- ESLint shows red/yellow squiggly lines for bugs and quality issues
- You save the file
- Prettier auto-formats the code (indentation, quotes, semicolons)
- ESLint auto-fix runs and fixes simple lint issues (unused imports,
var->const) - 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 ."
}
}
| Script | When to use | What it does |
|---|---|---|
npm run lint | CI pipeline, manual check | Reports all ESLint errors (exit code 1 if errors found) |
npm run lint:fix | Development | Auto-fixes what it can, reports the rest |
npm run format | Development | Reformats all files in place |
npm run format:check | CI pipeline | Checks 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
- ESLint finds bugs (unused vars, missing awaits, type coercion). Prettier formats code (indentation, quotes, semicolons). They solve different problems.
- Use
eslint-config-prettier+eslint-plugin-prettierto combine them without conflicts. - VS Code format-on-save gives instant feedback. lint-staged + Husky catches anything that slips through.
plugin:prettier/recommendedmust be the last entry in ESLint'sextendsarray.- lint-staged only checks staged files --- fast commits even in large codebases.
- The full pipeline: write -> save (Prettier) -> commit (lint-staged + commitlint) -> push (tests).
Explain-It Challenge
Explain without notes:
- What is the difference between what ESLint catches and what Prettier fixes? Give two examples of each.
- Why does
eslint-config-prettierneed to be the last item in theextendsarray? - A teammate commits code with a
console.logleft 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 ->