I've been reviewing the recent surge in "format on save" configurations across our team's VS Code setups, and I've noticed a pattern: nearly every developer now installs both ESLint and Prettier, often with `eslint-config-prettier` and `eslint-plugin-prettier` to manage the overlap. This has prompted me to reflect on a decade of maintaining JavaScript and TypeScript codebases, from monoliths migrated from on-prem to modern Kubernetes-hosted microservices. My contention is that we've overcomplicated our tooling. A meticulously configured ESLint is not merely a linter; it can and should be your formatter.
The core argument for a separate formatter like Prettier is its opinionated nature, which eliminates debates over code style. However, this comes at the cost of surrendering fine-grained control. In large, legacy migration projects I've led, we often have established patterns—specific object literal formatting, nuanced import sorting rules tied to our internal modules, or handling of non-standard file types—that Prettier either cannot handle or requires escape comments to bypass. ESLint, with its extensive plugin ecosystem, can be configured to enforce a consistent style *and* perform static analysis, all within a single execution pass.
Consider a typical `.eslintrc.js` for a TypeScript project. By leveraging the `--fix` capability, we can enforce formatting rules that are often cited as Prettier's domain.
```javascript
// .eslintrc.js
module.exports = {
parser: '@typescript-eslint/parser',
plugins: ['@typescript-eslint'],
extends: [
'eslint:recommended',
'plugin:@typescript-eslint/recommended',
],
rules: {
// Formatting-style rules enforced by ESLint
'indent': ['error', 2, { 'SwitchCase': 1 }],
'quotes': ['error', 'single', { 'avoidEscape': true }],
'semi': ['error', 'always'],
'comma-dangle': ['error', 'always-multiline'],
'object-curly-spacing': ['error', 'always'],
'array-bracket-spacing': ['error', 'never'],
'arrow-spacing': ['error', { 'before': true, 'after': true }],
// Import sorting can be handled by eslint-plugin-import
'import/order': ['error', { 'groups': ['builtin', 'external', 'internal', 'parent', 'sibling', 'index'] }],
// Enforce a consistent linebreak style, critical in cross-platform teams
'linebreak-style': ['error', 'unix'],
},
};
```
Running `eslint --fix .` will now correct indentation, quotes, semicolons, spacing, and import order. The primary arguments against this approach are performance and the fear of rule conflicts. Regarding performance, modern ESLint with caching is sufficiently fast for most codebases, and you only run one tool. Rule conflicts are a configuration discipline issue, not a tooling deficiency. A well-structured ESLint configuration, potentially using overrides for different file types, is a single source of truth.
The benefits of a single-tool approach are tangible:
* **Simplified Onboarding:** One configuration file, one set of dependencies, one set of IDE integrations.
* **Unified Fix Cycle:** A single `--fix` pass handles both logical errors *and* style violations. No need to orchestrate Prettier then ESLint.
* **Granular Control:** You can disable specific formatting rules for specific files or code patterns using ESLint's standard disable comments, which is more aligned with fine-grained linting than Prettier's broader-stroke approach.
* **Reduced Dependencies:** One less Node module chain in your `package.json`, which simplifies security audits, CI image maintenance, and migration paths.
I acknowledge Prettier's value in greenfield projects where "any consistent style is fine." However, in complex enterprise environments—precisely where I've spent my career—the ability to codify not just *a* style, but your organization's *specific* style, including non-standard patterns, within your linter is a significant advantage. The overhead of managing two tools and the plugins to make them coexist often outweighs the perceived benefit of separation. Invest the time in crafting a comprehensive ESLint configuration; it will serve as both your code quality guardrail and your style enforcer.