ESLint and Prettier solve different problems: ESLint catches code quality issues (unused variables, suspicious patterns, potential bugs), while Prettier enforces consistent formatting (indentation, line length, quote style). Together, they eliminate entire categories of code review friction — reviewers focus on logic, not style. But getting them to work together without conflicts has historically been painful.
This guide sets up a modern, conflict-free ESLint + Prettier configuration using ESLint's new flat config format, TypeScript support, React plugins, and automated enforcement with Husky and lint-staged pre-commit hooks.
Understanding the Division of Labor
Before configuring anything, understand what each tool does:
- Prettier: Format-only. It reformats your code into a consistent style. It is intentionally opinionated with few options. It does not catch bugs or enforce best practices.
-
ESLint: Linting + optional formatting. By default, some ESLint rules conflict with Prettier (e.g., both want to manage semicolons). The solution is
eslint-config-prettier, which disables all ESLint formatting rules, leaving that entirely to Prettier.
The old pattern of using eslint-plugin-prettier to run Prettier as an ESLint rule is now discouraged — it slows down ESLint and produces confusing error messages. The modern approach: run ESLint and Prettier as separate tools, with Prettier's formatting rules disabled in ESLint.
Installation
# Core tools
npm install -D eslint prettier
# Prettier integration — disables ESLint's formatting rules
npm install -D eslint-config-prettier
# TypeScript support
npm install -D @typescript-eslint/eslint-plugin @typescript-eslint/parser typescript-eslint
# React support (skip if not using React)
npm install -D eslint-plugin-react eslint-plugin-react-hooks eslint-plugin-jsx-a11y
# Import organization
npm install -D eslint-plugin-import eslint-plugin-simple-import-sort
# Pre-commit automation
npm install -D husky lint-staged
Prettier Configuration
Create a minimal .prettierrc — resist over-configuring, Prettier's defaults are well-chosen:
// .prettierrc
{
"semi": true,
"singleQuote": true,
"tabWidth": 2,
"trailingComma": "es5",
"printWidth": 100,
"bracketSpacing": true,
"arrowParens": "always"
}
# .prettierignore
node_modules
dist
build
.next
coverage
*.min.js
*.min.css
package-lock.json
pnpm-lock.yaml
yarn.lock
ESLint Flat Config (Modern Format)
ESLint 9 introduced the "flat config" system using eslint.config.js (or .mjs). The old .eslintrc.json format is deprecated. Here's a complete flat config for a TypeScript + React project:
// eslint.config.mjs
import js from '@eslint/js';
import tseslint from 'typescript-eslint';
import reactPlugin from 'eslint-plugin-react';
import reactHooksPlugin from 'eslint-plugin-react-hooks';
import jsxA11y from 'eslint-plugin-jsx-a11y';
import importPlugin from 'eslint-plugin-import';
import simpleImportSort from 'eslint-plugin-simple-import-sort';
import prettier from 'eslint-config-prettier';
export default tseslint.config(
// Base JavaScript rules
js.configs.recommended,
// TypeScript rules
...tseslint.configs.recommendedTypeChecked,
{
languageOptions: {
parserOptions: {
projectService: true,
tsconfigRootDir: import.meta.dirname,
},
},
},
// React rules
{
files: ['**/*.{jsx,tsx}'],
plugins: {
react: reactPlugin,
'react-hooks': reactHooksPlugin,
'jsx-a11y': jsxA11y,
},
rules: {
...reactPlugin.configs.recommended.rules,
...reactPlugin.configs['jsx-runtime'].rules, // For React 17+ JSX transform
...reactHooksPlugin.configs.recommended.rules,
...jsxA11y.configs.recommended.rules,
},
settings: {
react: { version: 'detect' },
},
},
// Import organization
{
plugins: {
import: importPlugin,
'simple-import-sort': simpleImportSort,
},
rules: {
'simple-import-sort/imports': 'error',
'simple-import-sort/exports': 'error',
'import/first': 'error',
'import/newline-after-import': 'error',
'import/no-duplicates': 'error',
},
},
// Custom project rules
{
rules: {
// TypeScript-specific
'@typescript-eslint/no-unused-vars': ['error', { argsIgnorePattern: '^_' }],
'@typescript-eslint/consistent-type-imports': ['error', { prefer: 'type-imports' }],
'@typescript-eslint/no-explicit-any': 'warn',
'@typescript-eslint/no-floating-promises': 'error',
'@typescript-eslint/await-thenable': 'error',
// General
'no-console': ['warn', { allow: ['warn', 'error'] }],
'prefer-const': 'error',
'no-var': 'error',
},
},
// Disable all formatting rules — Prettier handles these
prettier,
// Ignore patterns
{
ignores: [
'node_modules/**',
'dist/**',
'build/**',
'.next/**',
'coverage/**',
'*.config.js',
'*.config.mjs',
],
}
);
Node.js / Backend Config (No React)
// eslint.config.mjs — Node.js only
import js from '@eslint/js';
import tseslint from 'typescript-eslint';
import prettier from 'eslint-config-prettier';
export default tseslint.config(
js.configs.recommended,
...tseslint.configs.strictTypeChecked,
{
languageOptions: {
parserOptions: {
projectService: true,
tsconfigRootDir: import.meta.dirname,
},
},
},
{
rules: {
'@typescript-eslint/no-unused-vars': ['error', { argsIgnorePattern: '^_' }],
'@typescript-eslint/consistent-type-imports': ['error', { prefer: 'type-imports' }],
'@typescript-eslint/no-floating-promises': 'error',
'@typescript-eslint/no-misused-promises': 'error',
'no-console': ['warn', { allow: ['warn', 'error'] }],
'prefer-const': 'error',
},
},
prettier,
{ ignores: ['node_modules/**', 'dist/**', 'coverage/**'] }
);
Package.json Scripts
// package.json
{
"scripts": {
"lint": "eslint .",
"lint:fix": "eslint . --fix",
"format": "prettier --write .",
"format:check": "prettier --check .",
"check": "npm run lint && npm run format:check"
}
}
VS Code Integration
Install the ESLint and Prettier extensions, then configure VS Code to auto-format on save:
// .vscode/settings.json
{
"editor.defaultFormatter": "esbenp.prettier-vscode",
"editor.formatOnSave": true,
"editor.codeActionsOnSave": {
"source.fixAll.eslint": "explicit",
"source.organizeImports": "never" // Let simple-import-sort handle this
},
// Language-specific overrides
"[javascript]": {
"editor.defaultFormatter": "esbenp.prettier-vscode"
},
"[typescript]": {
"editor.defaultFormatter": "esbenp.prettier-vscode"
},
"[json]": {
"editor.defaultFormatter": "esbenp.prettier-vscode"
},
// ESLint settings
"eslint.useFlatConfig": true,
"eslint.validate": ["javascript", "javascriptreact", "typescript", "typescriptreact"]
}
// .vscode/extensions.json — recommend to all team members
{
"recommendations": [
"esbenp.prettier-vscode",
"dbaeumer.vscode-eslint",
"bradlc.vscode-tailwindcss" // If using Tailwind
]
}
Pre-Commit Hooks with Husky and lint-staged
Pre-commit hooks ensure code is always linted and formatted before it reaches the repository — regardless of editor settings:
# Initialize Husky
npx husky init
# This creates .husky/pre-commit with npx lint-staged
// package.json — add lint-staged config
{
"lint-staged": {
"*.{js,jsx,ts,tsx}": [
"eslint --fix",
"prettier --write"
],
"*.{json,md,css,scss,html,yaml,yml}": [
"prettier --write"
]
}
}
# .husky/pre-commit (created by husky init)
npx lint-staged
With this setup, every git commit automatically:
- Runs ESLint with auto-fix on staged JS/TS files
- Runs Prettier on staged files
- Adds the fixed files back to staging
- Aborts the commit if ESLint reports unfixable errors
CI/CD Integration
// .github/workflows/lint.yml
name: Lint
on:
pull_request:
push:
branches: [main]
jobs:
lint:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: '20'
cache: 'npm'
- run: npm ci
- run: npm run lint
- run: npm run format:check
Resolving Common Conflicts
"Prettier and ESLint disagree on formatting"
If you see ESLint errors about formatting after running Prettier, a formatting rule is not disabled. Check:
# Identify which ESLint rules conflict with Prettier
npx eslint-config-prettier path/to/file.ts
# You should see: "No rules that are unnecessary or conflict with Prettier were found."
# If conflicts are found, ensure prettier is LAST in your config extends
"TypeScript ESLint rules require type information"
Rules like @typescript-eslint/no-floating-promises require parserOptions.projectService: true. Without it, you get:
// Error: "Parsing error: parserOptions.project has been set..."
// Fix: Add to your eslint.config.mjs:
{
languageOptions: {
parserOptions: {
projectService: true,
tsconfigRootDir: import.meta.dirname,
},
},
}
"ESLint is slow on large projects"
Type-aware rules (anything using recommendedTypeChecked) are slower because they run the TypeScript compiler. Solutions:
- Use
eslint --cache— caches results per file, only re-lints changed files - Add
"ESLINT_USE_FLAT_CONFIG": "true"if you have a mix of configs - Profile with
TIMING=1 eslint .to see which rules are slowest
The Complete Workflow
With this setup, your daily workflow looks like:
- Write code — VS Code shows ESLint errors inline as you type
- Save file — Prettier formats automatically, ESLint auto-fixes safe issues
- Git commit — lint-staged runs ESLint + Prettier on staged files
-
Pull request — CI runs
npm run lintandformat:check, fails on issues
The result: consistent code style across your entire team, zero formatting debates in code review, and a codebase that catches common mistakes automatically.
For more on TypeScript configuration, see our guide on TypeScript vs JavaScript and Prettier vs ESLint — when to use which.
Free Developer Tools
If you found this article helpful, check out DevToolkit — 40+ free browser-based developer tools with no signup required.
Popular tools: JSON Formatter · Regex Tester · JWT Decoder · Base64 Encoder
🛒 Get the DevToolkit Starter Kit on Gumroad — source code, deployment guide, and customization templates.
Top comments (0)