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
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:
npm install --save-dev --save-exact eslint@10.13.0 @eslint/js@10.0.1 globals@17.13.0 prettier@3.9.10
added 80 packages, and audited 97 packages in 3s
28 packages are looking for funding
run `npm fund` for details
found 0 vulnerabilities
eslintis ESLint itself.@eslint/jsholds ESLint's recommended rules for JavaScript. The@eslint/part, read "at eslint slash", says the package belongs to the ESLint team.globalslists the names a browser or Node already provides, such asdocumentandconsole, so ESLint knows they're real.prettieris 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:
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 thedistfolder. It's the bundler's minified output, not code you wrote, and without this line ESLint reports dozens of problems in it.js.configs.recommendedturns 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.browsersays this code runs on a web page, sodocumentandwindoware real names. Code that runs in Node usesglobals.nodeinstead.rulesturns on two more rules. eqeqeq asks for===(read "triple equals", or "strictly equal") instead of==. prefer-const asks forconstwhen aletis 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:
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":
npx eslint .
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.
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:
npx prettier --check .
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:
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:
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:
"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:
- Install the two extensions. In VS Code's terminal, run
code --install-extension dbaeumer.vscode-eslintandcode --install-extension esbenp.prettier-vscode. ESLint's problems now show as squiggly underlines as you type, and hovering over one shows the rule. - 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.