on this page
- The one distinction that fixes everything
- Why they sometimes fight
- Format on save, so you stop thinking about it
- Project config: the rules live in the repo, not in your editor
- Running lint and format in CI, so the rules are not optional
- ESLint versus Prettier at a glance
- Classic traps
- Why any of this is worth the half hour
Open any real codebase and the code looks weirdly uniform. Same indentation everywhere, same quote style, same spacing around braces, and somehow nobody on the team seems to have fought about it. That is not because everyone agreed by hand. It is because two tools sit between the keyboard and the saved file, quietly fixing things, and you are about to set both of them up.
Those tools are ESLint and Prettier. People say the names together so often that they blur into one fuzzy “code quality” thing, and that blur is exactly why they end up misconfigured. They do two different jobs. Once you can tell the jobs apart, the whole setup stops being mysterious.
The one distinction that fixes everything
Here is the line, and it is worth memorizing:
Prettier handles how your code looks. ESLint handles whether your code is sketchy.
Prettier is a formatter. It does not read your code for meaning and it genuinely does not care whether your program works. It cares about spacing, quotes, line breaks, semicolons, and trailing commas. You hand it a messy file, it reshapes the whole thing into one consistent style and hands it back. That is the entire job. A formatter will happily format code that crashes on the first line, because “does this run” was never its question.
ESLint is a linter. It actually reads your code looking for likely bugs and bad patterns: a variable you declared and never used, a Promise you forgot to await, a == where you almost certainly meant ===, a React hook called inside an if. It flags those, and for a good chunk of them it can auto-fix the problem. ESLint is the one that occasionally saves you from a bug at 2am.
So they are not competitors. One is a stylist, the other is a code reviewer that never gets tired. You want both.
# Prettier reformats the file in place
npx prettier --write src/app.ts
# ESLint reports problems (and fixes the auto-fixable ones)
npx eslint src/app.ts --fix
The first command rewrites src/app.ts so the spacing and quotes match one style. The second scans the same file for suspicious patterns and repairs the ones it safely can. Run them on the same file and you can watch each tool stay in its own lane: Prettier touches the whitespace, ESLint touches the logic.
Why they sometimes fight
If they have separate jobs, why does every tutorial mention them fighting? History.
For years ESLint shipped formatting rules of its own. It had opinions about indentation and quotes and semicolons, right alongside its real job of catching bugs. So if you turned those rules on and also ran Prettier, you had two tools both trying to own your spacing. Prettier would format a line one way, ESLint would yell that the line was wrong, you would “fix” it, and Prettier would undo your fix on the next save. That is the fight: not a bug, just two tools given the same job and no referee.
The fix is to pick a referee. You let Prettier own everything about formatting, and you switch off ESLint’s stylistic rules so it stops having opinions about looks. There is a package built for exactly this,
Format on save, so you stop thinking about it
Running prettier --write by hand on every file would be miserable, and you would forget. The point of a formatter is that it is automatic.
In VS Code you turn on format on save: every time you hit save, Prettier reformats the file before it writes to disk. You stop caring about spacing entirely. You paste in some gnarly snippet from Stack Overflow with random indentation, save, and it snaps into your project’s style. The whole thing is two editor settings:
// .vscode/settings.json
{
"editor.formatOnSave": true,
"editor.defaultFormatter": "esbenp.prettier-vscode"
}
editor.formatOnSave tells the editor to format before saving, and editor.defaultFormatter says use the Prettier extension to do it. Commit this .vscode/settings.json and everyone who opens the project gets format on save without configuring anything. New teammate clones the repo, opens it, starts saving correctly formatted files on day one.
Project config: the rules live in the repo, not in your editor
Here is the part beginners skip, and it is the part that actually matters on a team.
Your formatting and linting rules need to live in files committed to the repository, not in your personal editor settings. Why? Because if the rules live in your editor, they are your rules. The next person has different editor defaults, formats a file their way, and now every pull request is a war between two invisible config setups. Put the config in the repo and there is one source of truth that every machine and every CI run reads from.
Prettier reads a .prettierrc file from the project root:
// .prettierrc
{
"semi": true,
"singleQuote": false,
"trailingComma": "all",
"printWidth": 100
}
That config says: keep semicolons, use double quotes, add trailing commas everywhere it is valid, and wrap lines around 100 characters. The actual values matter less than the fact that they are written down and shared. Pick something, commit it, move on. The whole point of Prettier is that you stop arguing about these and let the file decide.
ESLint reads its own config, usually eslint.config.js in modern setups (this newer style is called flat config):
// eslint.config.js
import js from "@eslint/js";
import prettier from "eslint-config-prettier";
export default [
js.configs.recommended,
{
rules: {
"no-unused-vars": "warn",
"eqeqeq": "error",
},
},
prettier, // turns off ESLint's formatting rules; keep this last
];
This starts from ESLint’s recommended rules, warns you about unused variables, makes == an error so you are pushed toward ===, and then applies eslint-config-prettier last so any leftover style rules get switched off. Order matters: that prettier entry goes at the end so it overrides the formatting rules the earlier configs turned on.
Add the same commands to package.json so they are not just editor magic:
// package.json
{
"scripts": {
"lint": "eslint .",
"format": "prettier --check ."
}
}
Now npm run lint checks the whole project for code problems, and npm run format checks whether everything is formatted (the --check flag reports unformatted files instead of rewriting them, which is what you want in an automated check). These are the exact commands your CI is about to run, which is the next piece.
Running lint and format in CI, so the rules are not optional
Committing the config gets everyone using the same rules locally. But “locally” depends on people remembering to run the tools, and people forget. The rules are only real if something enforces them automatically. That something is CI.
CI stands for continuous integration: a service that runs commands against your code every time you push or open a pull request. The most common one for repos on GitHub is
# .github/workflows/quality.yml
name: Code quality
on: pull_request
jobs:
check:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 20
- run: npm ci
- run: npm run format
- run: npm run lint
Walking through it: this triggers on every pull request, spins up a fresh Linux machine, checks out your code, installs Node, runs npm ci to install the exact dependencies from your lockfile, then runs your format check and your lint check. If either command exits with an error, the whole job goes red and GitHub marks the PR as failing.
The effect is quiet but real. Nobody has to leave a comment saying “please run Prettier” on someone’s pull request, because an unformatted PR fails before a human even looks at it. The main branch stays clean and consistent without anyone playing style cop. The rules went from polite suggestions to actual gates.
If your CI needs secrets or environment values to run the rest of your pipeline, that is a separate topic covered in Environment variables explained. For pure lint and format checks like these, you usually need none.
ESLint versus Prettier at a glance
When you forget which tool does what, this is the table to come back to:
| ESLint (linter) | Prettier (formatter) | |
|---|---|---|
| What it checks | Likely bugs and bad patterns: unused vars, missing await, == vs ===, misused hooks | How the code looks: spacing, quotes, line breaks, trailing commas |
| Can it auto-fix? | Some rules, with --fix | Yes, that is the whole job |
| What it does not do | Will not enforce a consistent visual style (let Prettier do that) | Will not tell you your code is buggy or wrong |
| Config file | eslint.config.js | .prettierrc |
| Mental model | Is this code sketchy? | Does this code look consistent? |
Classic traps
Most ESLint and Prettier pain comes from the same handful of setups. Here they are, and how each one bites you.
| Trap | What goes wrong | Fix |
|---|---|---|
| Both tools format | ESLint and Prettier fight over spacing and undo each other on save | Let Prettier format, add eslint-config-prettier to disable ESLint’s style rules |
| No committed config | Each person’s editor defaults win, so every diff is a style war | Commit .prettierrc and eslint.config.js to the repo |
| Format on save off | People forget to format, PRs arrive as whitespace soup | Turn on editor.formatOnSave and commit .vscode/settings.json |
| No CI check | The rules become optional suggestions nobody enforces | Run prettier --check and eslint in GitHub Actions on every PR |
| Manual-only linting | Bugs ESLint would have caught slip through because nobody ran it | Make the CI check required so the PR cannot merge while it is red |
The thread running through all of these: a rule that only some people follow is not a rule. Either the machine enforces it on every change, or it is just a thing you wish were true.
Why any of this is worth the half hour
Step back from the config files for a second. All of this exists to do two boring, valuable things.
First, it kills noise. When formatting is automatic and identical for everyone, a diff in a pull request shows what actually changed: the logic, the new function, the fixed bug. It does not show that someone’s editor converted forty lines from tabs to spaces, burying the one real change in a wall of meaningless edits. Reviewers see the substance. That alone is worth the setup.
Second, it catches dumb bugs early, while they are cheap. An unused variable or a forgotten await is much nicer to find on your own machine, with a squiggly underline, than in production three weeks later. ESLint is not going to catch deep logic errors, and it would be lying to promise that. It catches the careless stuff, and careless stuff is most of what breaks.
None of this is about winning a style argument. The whole reason Prettier exists is so the argument never happens: there is one style, the tool applies it, and the energy you would have spent on tabs-versus-spaces goes into the actual product instead.
When you ship a project to .prettierrc and the ESLint config and the CI checks are already there waiting for you. You save a file, it formats itself to match everyone else, and you fit into a codebase you have never seen without anyone having to explain the house style. That is the quiet payoff: less arguing, fewer dumb bugs, and code that reads like one person wrote it even when twenty people did.