# Clean Code with ESLint and Prettier

ESLint reads your code and flags likely mistakes, like an unused variable or a == that should be ===, and Prettier formats every file the same way, so you set them up once with a flat config and npm scripts and let VS Code run them as you save.

Journeyman · JavaScript · Node.js · Software Development · Updated November 1, 2026

## After this lesson you can

- Set up ESLint with a flat config in eslint.config.js and fix the problems it reports
- Format code with Prettier from an npm script and on save in VS Code, and tell what Prettier changes from what ESLint reports

## Key takeaways

- A linter such as ESLint finds likely mistakes without running the code. A formatter such as Prettier only changes how code looks, never what it does.
- ESLint's settings live in eslint.config.js, a flat config. It lists rule sets such as js.configs.recommended and single rules such as eqeqeq.
- Each problem ESLint reports gives the line and column, a message and the rule's name. Fix the code itself; don't switch the rule off.
- prettier --check . lists the files that need formatting, and prettier --write . formats them. Teams run both tools from npm scripts.
- With the Prettier extension and Format On Save turned on, VS Code formats each file every time you save it.

On a team, your code is read before it's used. Someone reviews every change, and the first things a reviewer spots are the small ones: a variable nobody uses, a `==` that should be `===`, quotes of two kinds in one file. On many teams, code can't even be merged until two tools say it's clean. Those two tools are ESLint and Prettier. Set them up once, and they catch those small things for you, before anyone else sees them, in every project from a class assignment to a company's website.

## A spell-checker and an auto-format button

A word processor helps you in two different ways. Its spell-checker underlines words that look wrong: it doesn't change anything, it points, and you decide. Its formatting tools set the fonts, the spacing and the margins the same way everywhere, so you stop thinking about them.

Code has the same two helpers:

- A **linter** is the spell-checker. It reads your code without running it and points out likely mistakes: a variable you made and never used, a name that's misspelled, a comparison that can surprise you. **ESLint** (said "E-S-lint") is the linter for JavaScript.
- A **code formatter** is the formatting button. It rewrites how the code is laid out (quotes, semicolons, spaces, indents, line breaks) without changing what it does. **Prettier** is the formatter most JavaScript projects use.

They don't overlap. ESLint doesn't care how many spaces you use, and Prettier never changes what your code does.

## Install the tools

In the `game-night-scoreboard` project from *A Dev Server and Build with Vite*, install four packages as devDependencies. `--save-exact` writes each version without a `^`, so everyone on the project gets exactly the same rules:

```bash
npm install --save-dev --save-exact eslint@10.13.0 @eslint/js@10.0.1 globals@17.13.0 prettier@3.9.10
```

```text
added 80 packages, and audited 97 packages in 3s

28 packages are looking for funding
  run `npm fund` for details

found 0 vulnerabilities
```

- `eslint` is ESLint itself.
- `@eslint/js` holds ESLint's recommended rules for JavaScript. The `@eslint/` part, read "at eslint slash", says the package belongs to the ESLint team.
- `globals` lists the names a browser or Node already provides, such as `document` and `console`, so ESLint knows they're real.
- `prettier` is Prettier.

Four packages went in, and 80 arrived: the tools need packages of their own, and npm installs those too.

## ESLint's settings: eslint.config.js

ESLint reads its settings from a file named `eslint.config.js` at the top of the project. Make it with this:

```javascript
import js from "@eslint/js";
import globals from "globals";
import { defineConfig, globalIgnores } from "eslint/config";

export default defineConfig([
  globalIgnores(["dist"]),
  js.configs.recommended,
  {
    languageOptions: {
      globals: globals.browser,
    },
    rules: {
      eqeqeq: "error",
      "prefer-const": "error",
    },
  },
]);
```

This is called a **flat config**: one flat list, in square brackets, of settings that apply in order. Each entry does one thing:

- **`globalIgnores(["dist"])`** tells ESLint to skip the `dist` folder. It's the bundler's minified output, not code you wrote, and without this line ESLint reports dozens of problems in it.
- **`js.configs.recommended`** turns on the rules the ESLint team recommends for everyone, such as **no-unused-vars** (a variable that's never used) and **no-undef** (a name that was never declared).
- **`globals.browser`** says this code runs on a web page, so `document` and `window` are real names. Code that runs in Node uses `globals.node` instead.
- **`rules`** turns on two more rules. **eqeqeq** asks for `===` (read "triple equals", or "strictly equal") instead of `==`. **prefer-const** asks for `const` when a `let` is never given a new value. Each rule is set to `"error"`, `"warn"` or `"off"`.

## Read what ESLint reports

Add a `winner` function to the end of `src/scores.js`. It works, but it has three things a reviewer would flag:

```javascript
export function winner(players, rounds) {
  let table = standings(players, rounds);
  const roundsPlayed = rounds.length;
  if (table[0].total == table[1].total) {
    return "It's a tie!";
  }
  return table[0].player;
}
```

`npx` (read "en-pee-ex") runs a command from a package installed in the project. Run ESLint on every file in the project; the `.` (read "dot") means "this folder and everything in it":

```bash
npx eslint .
```

```text
C:\Users\...\game-night-scoreboard\src\scores.js
  20:7   error  'table' is never reassigned. Use 'const' instead   prefer-const
  21:9   error  'roundsPlayed' is assigned a value but never used  no-unused-vars
  22:22  error  Expected '===' and instead saw '=='                eqeqeq

✖ 3 problems (3 errors, 0 warnings)
  1 error and 0 warnings potentially fixable with the `--fix` option.
```

Each problem has four parts: the line and column (`20:7` is line 20, character 7), how serious it is, what's wrong, and the name of the rule. Fix each one in the code:

| Rule | What it caught | The fix |
|---|---|---|
| prefer-const | `table` never changes after it's made | Change `let table` to `const table` |
| no-unused-vars | `roundsPlayed` is never used | Delete the line |
| eqeqeq | `==` converts types before comparing: `0 == ""` is `true` | Change `==` to `===` |

Why does `==` matter? As in *Booleans and Comparisons*, `==` changes the types until they match, so `"7" == 7` is `true`, and so is `0 == ""`. With scores typed into a form, a player's `0` and an empty box would count as a tie. Run `npx eslint .` again, and it prints nothing at all: no news is good news. `npx eslint . --fix` fixes some rules, such as prefer-const, by itself. Read what it changed.

## Prettier: format, don't argue

Here's the same `winner` function, typed in a hurry: crooked indents, no spaces, single quotes, no semicolons.

```javascript
export function winner(players,rounds){
    const table=standings(players,rounds)
  if(table[0].total===table[1].total){ return 'It\'s a tie!' }
    return table[0].player
}
```

ESLint finds nothing wrong with it: none of its rules is about layout. That's Prettier's job. `--check` lists the files Prettier would change, and changes nothing:

```bash
npx prettier --check .
```

```text
Checking formatting...
[warn] src/scores.js
[warn] Code style issues found in the above file. Run Prettier with --write to fix.
```

`npx prettier --write .` formats them. It names each file it read, with *(unchanged)* after those that were already fine, and leaves out whatever `.gitignore` lists, such as `node_modules` and `dist`:

```text
eslint.config.js 17ms (unchanged)
index.html 13ms (unchanged)
package-lock.json 15ms (unchanged)
package.json 1ms (unchanged)
src/main.js 6ms (unchanged)
src/scores.js 5ms
src/style.css 11ms (unchanged)
```

`src/scores.js` is formatted:

```javascript
export function winner(players, rounds) {
  const table = standings(players, rounds);
  if (table[0].total === table[1].total) {
    return "It's a tie!";
  }
  return table[0].player;
}
```

Teams that use Prettier stop arguing about tabs, spaces and quotes: the tool decides, the same way for everyone. (A `.prettierrc` file can change a few of its choices, such as `{ "singleQuote": true }`, but most projects keep the defaults.)

## npm scripts and format on save

Teams don't type these commands from memory: they're npm scripts. Add three to `package.json`, next to Vite's:

```json
  "scripts": {
    "dev": "vite",
    "build": "vite build",
    "preview": "vite preview",
    "lint": "eslint .",
    "format": "prettier --write .",
    "format:check": "prettier --check ."
  },
```

Now `npm run lint`, `npm run format` and `npm run format:check` work the same on every computer. `format:check` changes nothing, so it's the one an automatic check runs before code is merged.

Better still, let VS Code run the tools while you type:

1. **Install the two extensions.** In VS Code's terminal, run `code --install-extension dbaeumer.vscode-eslint` and `code --install-extension esbenp.prettier-vscode`. ESLint's problems now show as squiggly underlines as you type, and hovering over one shows the rule.
2. **Turn on Format On Save.** Open **File > Preferences > Settings** (**Code > Settings** on a Mac), search for *format on save* and tick **Editor: Format On Save**. Then search for *default formatter* and choose **Prettier - Code formatter**.

Now every save formats the file. Type messy, press Ctrl+S, and it's tidy.

**Try it.** Turn on Format On Save, open `src/main.js`, and mess up its indents and quotes. Save, and watch Prettier put them back. Then add `let unused = 5;` to the file and look for the squiggle.

## Check yourself

2 questions to try on the lesson page.

## So what

ESLint catches the mistakes that slip past you, and Prettier makes every file look like the same careful person wrote it. With both running on save, you hand in code that's ready for review, every time.

## Key terms

- [Linter](https://www.ask-codey.com/glossary/linter)
- [ESLint](https://www.ask-codey.com/glossary/eslint)
- Prettier

## Cheat sheets

- [Clean Code with ESLint and Prettier](https://www.ask-codey.com/cheat-sheets/javascript-tooling-eslint-and-prettier-cheat-sheet)

## Related

- [npm and Packages](https://www.ask-codey.com/lessons/javascript-tooling-npm-and-packages)
- [Exercise: Game Night Project Setup](https://www.ask-codey.com/lessons/exercise-javascript-game-night-project-setup)

Source: https://www.ask-codey.com/lessons/javascript-tooling-eslint-and-prettier
