DEV Community

楊東霖
楊東霖

Posted on • Originally published at devtoolkit.cc

ESLint + Prettier Setup: The Complete Developer Configuration Guide (2026)

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
Enter fullscreen mode Exit fullscreen mode

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"
}
Enter fullscreen mode Exit fullscreen mode
# .prettierignore
node_modules
dist
build
.next
coverage
*.min.js
*.min.css
package-lock.json
pnpm-lock.yaml
yarn.lock
Enter fullscreen mode Exit fullscreen mode

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',
    ],
  }
);
Enter fullscreen mode Exit fullscreen mode

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/**'] }
);
Enter fullscreen mode Exit fullscreen mode

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"
  }
}
Enter fullscreen mode Exit fullscreen mode

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"]
}
Enter fullscreen mode Exit fullscreen mode
// .vscode/extensions.json  recommend to all team members
{
  "recommendations": [
    "esbenp.prettier-vscode",
    "dbaeumer.vscode-eslint",
    "bradlc.vscode-tailwindcss"  // If using Tailwind
  ]
}
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode
// 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"
    ]
  }
}
Enter fullscreen mode Exit fullscreen mode
# .husky/pre-commit (created by husky init)
npx lint-staged
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

"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,
    },
  },
}
Enter fullscreen mode Exit fullscreen mode

"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 lint and format: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)