
Automating Code Quality: The Modern Stack with ESLint, Prettier, and Husky
A comprehensive guide to setting up a robust, automated code quality environment. We cover the transition to ESLint Flat Config, integrating Prettier for styling, and using Husky to enforce checks before every commit.
The Cost of Manual Consistency
In the world of micro-SaaS development and automation engineering, decision fatigue is the enemy. Every second you spend debating whether to use single quotes or double quotes, or manual-fixing indentation in a code review, is a second stolen from building actual logic.
As developers, we like to think we are paid to write code. We aren't. We are paid to solve problems. Formatting syntax is not a problem that requires human cognition. It is a heuristic that machines handle better than we do.
When I bootstrap a new intelligent agent or a SaaS boilerplate, the first thing I implement isn't the database schema—it's the automated quality pipeline. By standardizing the environment immediately, we eliminate an entire category of technical debt: inconsistency.
This guide details the exact setup I use to automate code quality. We will cover the modern ESLint (Flat Config), Prettier integration, and how to force these rules using Husky and lint-staged.
The Distinction: Linting vs. Formatting
Before modifying configuration files, we must clarify the separation of concerns. A common mistake in junior developer setups is conflating these two tools.
- Prettier (The Formatter): It cares about how your code looks. It handles line wrapping, indentation, quotes, and commas. It is opinionated and dumb. It does not care if your variable is unused; it only cares that the line length is under 80 characters.
- ESLint (The Linter): It cares about how your code works. It catches bugs, unused variables, infinite loops, and bad practices. While it can handle formatting, we intentionally disable those features to let Prettier do its job.
The goal of this automation is simple: Prettier handles the style, ESLint handles the quality, and the environment enforces both.
Step 1: The Modern Prettier Configuration
Prettier is the easiest part of the stack because it requires almost no thinking. We want to install it and create a configuration file that dictates the visual style of the project.
Installation
npm install --save-dev prettierConfiguration (.prettierrc)
Create a .prettierrc file in your root directory. Here is the configuration I use for TypeScript and Node.js projects. It prioritizes readability and standard JS conventions.
{
"trailingComma": "es5",
"tabWidth": 2,
"semi": true,
"singleQuote": true,
"printWidth": 80,
"bracketSpacing": true,
"endOfLine": "lf"
}Why these settings?
singleQuote: Reduces visual noise compared to double quotes.trailingComma: Essential for clean git diffs. When you add a new item to an object or array, you only change one line, not two.endOfLine: Enforcing 'lf' prevents the dreaded Windows (CRLF) vs. Linux/Mac (LF) carriage return conflicts in version control.
Step 2: ESLint with Flat Config (The New Standard)
ESLint has undergone a major shift. The old .eslintrc format is deprecated. The new standard is the Flat Config system used in eslint.config.mjs. Many tutorials still reference the old way; if you are building in 2024 and beyond, use Flat Config.
Installation
npm install --save-dev eslint @eslint/js globalsIf you are using TypeScript (which you should be), you also need the typescript-eslint packages:
npm install --save-dev typescript-eslintConfiguration (eslint.config.mjs)
Create eslint.config.mjs at your root. This file exports an array of configuration objects. It allows for much more granular control than the old cascading config.
import globals from "globals";
import pluginJs from "@eslint/js";
import tseslint from "typescript-eslint";
export default [
{ files: ["**/*.{js,mjs,cjs,ts}"] },
{ languageOptions: { globals: globals.node } },
pluginJs.configs.recommended,
...tseslint.configs.recommended,
{
rules: {
"no-unused-vars": "warn",
"no-console": "warn",
"prefer-const": "error",
"eqeqeq": ["error", "always"]
}
}
];This configuration does three things:
- Applies recommended JavaScript rules.
- Applies recommended TypeScript rules.
- Overrides specific rules. I enforce
eqeqeqto prevent type coercion bugs andprefer-constto ensure immutability where possible.
Step 3: Resolving Conflicts (The Critical Step)
If you run both tools now, they will fight. ESLint might throw an error because of a missing semicolon, and Prettier will try to add it. You need to tell ESLint to back off on styling rules.
We use eslint-config-prettier to turn off all ESLint rules that are unnecessary or might conflict with Prettier.
npm install --save-dev eslint-config-prettierUpdate your eslint.config.mjs to include the prettier config at the very end of the array. Order matters here—the last config wins.
import globals from "globals";
import pluginJs from "@eslint/js";
import tseslint from "typescript-eslint";
import eslintConfigPrettier from "eslint-config-prettier"; // Import this
export default [
{ files: ["**/*.{js,mjs,cjs,ts}"] },
{ languageOptions: { globals: globals.node } },
pluginJs.configs.recommended,
...tseslint.configs.recommended,
eslintConfigPrettier // Add this last
];Step 4: VS Code Environment Automation
Tools are useless if they aren't integrated into your workflow. We don't want to run a CLI command to format code; we want it to happen when we hit Ctrl + S.
Create a .vscode/settings.json file in your project root. This ensures that anyone cloning your repo (or you on a different machine) gets the exact same behavior without manual setup.
{
"editor.formatOnSave": true,
"editor.defaultFormatter": "esbenp.prettier-vscode",
"editor.codeActionsOnSave": {
"source.fixAll.eslint": "explicit"
},
"eslint.validate": [
"javascript",
"javascriptreact",
"typescript",
"typescriptreact"
]
}This configuration creates the "Save -> Format -> Fix" loop. When you save, Prettier formats the code, and ESLint fixes auto-fixable logical errors.
Step 5: Enforcing the Rules with Husky & Lint-Staged
Here is where we move from "configuring tools" to "automating systems."
You cannot rely on willpower to maintain code quality. If you are in a rush to push a hotfix, you will skip the lint command. We need a gatekeeper. That gatekeeper is Husky.
Husky uses Git hooks to run scripts before specific Git actions, like commit or push.
Install Husky
npm install --save-dev husky
npx husky initThis creates a .husky folder. We want to edit the pre-commit hook. However, running formatting on the entire project every commit is slow and inefficient. We only want to check the files that actually changed.
Install Lint-Staged
Lint-staged allows us to run linters against staged git files only.
npm install --save-dev lint-stagedConfigure Package.json
Add the following configuration to your package.json:
"lint-staged": {
"**/*.{js,ts,jsx,tsx}": [
"eslint --fix",
"prettier --write"
],
"**/*.{json,css,md}": [
"prettier --write"
]
}Now, update the Husky pre-commit hook (.husky/pre-commit) to run lint-staged:
npx lint-stagedThe Result
Here is the new workflow:
- You make messy changes to a TypeScript file.
- You run
git add . - You run
git commit -m "feat: new agent logic" - Husky intercepts the commit.
- It triggers lint-staged.
- Lint-staged runs ESLint and Prettier only on the changed files.
- If there is an unfixable error (like a used variable), the commit fails.
- If it fixes formatting, it updates the files and allows the commit.
Conclusion: The Cognitive Payoff
Automation isn't just about speed; it's about reliability and mental clarity. By implementing this pipeline, you remove the need to discuss formatting in code reviews entirely.
When I look at a Pull Request for one of my tools, I know for a fact that the code compiles, the formatting is consistent, and basic best practices are followed. This allows me to focus on the architecture and the business logic.
Set this up once, create a template from it, and never configure it again. Build systems, don't just write code.
Comments
Loading comments...