
Introduction
Maintaining consistent code style and quality across a development team is challenging, especially in large TypeScript or JavaScript projects with multiple contributors. Inconsistent formatting leads to noisy diffs, merge conflicts, and endless code review discussions about tabs versus spaces. Combining Prettier for automatic code formatting with ESLint for code quality enforcement provides an elegant solution that automates style consistency while catching bugs and enforcing best practices.
In this comprehensive guide, you’ll learn how to configure Prettier and ESLint together for modern TypeScript and JavaScript projects, including the new ESLint flat config format, integration with popular frameworks, CI/CD automation, and advanced customization patterns.
Understanding the Difference Between Prettier and ESLint
Although both tools improve code quality, they serve fundamentally different purposes. Understanding this distinction is crucial for proper configuration.
Prettier: The Opinionated Code Formatter
Prettier handles code formatting exclusively:
- Line length and wrapping
- Indentation (tabs vs. spaces)
- Quotes (single vs. double)
- Semicolons
- Trailing commas
- Bracket spacing
- JSX formatting
ESLint: The Pluggable Linter
ESLint handles code correctness and quality:
- Unused variables and imports
- Undefined references
- Type safety issues
- Security vulnerabilities
- Best practice violations
- Framework-specific rules
Modern Setup with ESLint Flat Config
ESLint 9+ uses a new flat config format (eslint.config.js) that’s simpler and more powerful than the legacy .eslintrc format.
Step 1: Install Dependencies
# Core packages
npm install --save-dev eslint prettier
# TypeScript support
npm install --save-dev typescript-eslint @typescript-eslint/parser
# Prettier integration
npm install --save-dev eslint-config-prettier eslint-plugin-prettier
# Optional but recommended
npm install --save-dev eslint-plugin-import eslint-plugin-unused-imports
Step 2: Create ESLint Flat Config
// eslint.config.js - Modern Flat Config
import js from '@eslint/js';
import typescript from 'typescript-eslint';
import prettier from 'eslint-plugin-prettier/recommended';
import importPlugin from 'eslint-plugin-import';
import unusedImports from 'eslint-plugin-unused-imports';
export default typescript.config(
// Base JavaScript rules
js.configs.recommended,
// TypeScript rules
...typescript.configs.recommended,
...typescript.configs.stylistic,
// Prettier integration (must be last)
prettier,
// Global ignores
{
ignores: [
'node_modules/**',
'dist/**',
'build/**',
'coverage/**',
'*.config.js',
'*.config.mjs',
],
},
// Custom rules for all files
{
plugins: {
'import': importPlugin,
'unused-imports': unusedImports,
},
rules: {
// Prettier enforced as error
'prettier/prettier': 'error',
// TypeScript rules
'@typescript-eslint/no-unused-vars': 'off', // Handled by unused-imports
'@typescript-eslint/no-explicit-any': 'warn',
'@typescript-eslint/explicit-function-return-type': 'off',
'@typescript-eslint/explicit-module-boundary-types': 'off',
'@typescript-eslint/no-non-null-assertion': 'warn',
'@typescript-eslint/consistent-type-imports': [
'error',
{ prefer: 'type-imports' }
],
// Import rules
'import/order': [
'error',
{
groups: [
'builtin',
'external',
'internal',
['parent', 'sibling'],
'index',
'type',
],
'newlines-between': 'always',
alphabetize: { order: 'asc', caseInsensitive: true },
},
],
'import/no-duplicates': 'error',
// Unused imports (auto-fixable)
'unused-imports/no-unused-imports': 'error',
'unused-imports/no-unused-vars': [
'warn',
{
vars: 'all',
varsIgnorePattern: '^_',
args: 'after-used',
argsIgnorePattern: '^_',
},
],
// General best practices
'no-console': ['warn', { allow: ['warn', 'error'] }],
'no-debugger': 'error',
'prefer-const': 'error',
'no-var': 'error',
'eqeqeq': ['error', 'always'],
},
},
// Test file overrides
{
files: ['**/*.test.ts', '**/*.spec.ts', '**/__tests__/**'],
rules: {
'@typescript-eslint/no-explicit-any': 'off',
'no-console': 'off',
},
}
);
Step 3: Configure Prettier
// prettier.config.js - Prettier Configuration
/** @type {import('prettier').Config} */
export default {
// Line width
printWidth: 100,
// Indentation
tabWidth: 2,
useTabs: false,
// Quotes
singleQuote: true,
jsxSingleQuote: false,
// Semicolons
semi: true,
// Trailing commas (ES5+ compatible)
trailingComma: 'es5',
// Brackets
bracketSpacing: true,
bracketSameLine: false,
// Arrow functions
arrowParens: 'always',
// Line endings
endOfLine: 'lf',
// Prose wrapping for markdown
proseWrap: 'preserve',
// HTML whitespace sensitivity
htmlWhitespaceSensitivity: 'css',
// Embedded language formatting
embeddedLanguageFormatting: 'auto',
// Single attribute per line in HTML/JSX
singleAttributePerLine: false,
// Overrides for specific file types
overrides: [
{
files: '*.json',
options: {
printWidth: 80,
tabWidth: 2,
},
},
{
files: '*.md',
options: {
proseWrap: 'always',
printWidth: 80,
},
},
{
files: '*.yaml',
options: {
tabWidth: 2,
singleQuote: false,
},
},
],
};
Step 4: Configure Ignore Files
# .prettierignore - Files to skip formatting
node_modules/
dist/
build/
coverage/
*.min.js
*.min.css
package-lock.json
yarn.lock
pnpm-lock.yaml
.next/
.nuxt/
.output/
Step 5: Add NPM Scripts
{
"scripts": {
"lint": "eslint .",
"lint:fix": "eslint . --fix",
"format": "prettier --write .",
"format:check": "prettier --check .",
"typecheck": "tsc --noEmit",
"validate": "npm run typecheck && npm run lint && npm run format:check"
}
}
Framework-Specific Configurations
React/Next.js Configuration
# Install React-specific plugins
npm install --save-dev eslint-plugin-react eslint-plugin-react-hooks eslint-plugin-jsx-a11y
// eslint.config.js - React Configuration
import js from '@eslint/js';
import typescript from 'typescript-eslint';
import react from 'eslint-plugin-react';
import reactHooks from 'eslint-plugin-react-hooks';
import jsxA11y from 'eslint-plugin-jsx-a11y';
import prettier from 'eslint-plugin-prettier/recommended';
export default typescript.config(
js.configs.recommended,
...typescript.configs.recommended,
// React configuration
{
files: ['**/*.{jsx,tsx}'],
plugins: {
'react': react,
'react-hooks': reactHooks,
'jsx-a11y': jsxA11y,
},
languageOptions: {
parserOptions: {
ecmaFeatures: {
jsx: true,
},
},
},
settings: {
react: {
version: 'detect',
},
},
rules: {
// React rules
...react.configs.recommended.rules,
...react.configs['jsx-runtime'].rules,
'react/prop-types': 'off', // Using TypeScript
'react/react-in-jsx-scope': 'off', // React 17+
'react/jsx-no-target-blank': 'error',
'react/jsx-curly-brace-presence': [
'error',
{ props: 'never', children: 'never' }
],
'react/self-closing-comp': 'error',
'react/jsx-sort-props': [
'warn',
{
callbacksLast: true,
shorthandFirst: true,
reservedFirst: true,
},
],
// React Hooks rules
...reactHooks.configs.recommended.rules,
'react-hooks/rules-of-hooks': 'error',
'react-hooks/exhaustive-deps': 'warn',
// Accessibility rules
...jsxA11y.configs.recommended.rules,
'jsx-a11y/anchor-is-valid': [
'error',
{
components: ['Link'],
specialLink: ['hrefLeft', 'hrefRight'],
aspects: ['invalidHref', 'preferButton'],
},
],
},
},
prettier
);
Vue.js Configuration
# Install Vue-specific plugins
npm install --save-dev eslint-plugin-vue vue-eslint-parser
// eslint.config.js - Vue Configuration
import js from '@eslint/js';
import typescript from 'typescript-eslint';
import vue from 'eslint-plugin-vue';
import vueParser from 'vue-eslint-parser';
import prettier from 'eslint-plugin-prettier/recommended';
export default [
js.configs.recommended,
...typescript.configs.recommended,
// Vue configuration
{
files: ['**/*.vue'],
plugins: {
vue,
},
languageOptions: {
parser: vueParser,
parserOptions: {
parser: typescript.parser,
extraFileExtensions: ['.vue'],
ecmaVersion: 'latest',
sourceType: 'module',
},
},
rules: {
...vue.configs['vue3-recommended'].rules,
'vue/multi-word-component-names': 'off',
'vue/no-v-html': 'warn',
'vue/require-default-prop': 'off',
'vue/component-tags-order': [
'error',
{
order: ['script', 'template', 'style'],
},
],
'vue/block-lang': [
'error',
{
script: { lang: 'ts' },
},
],
'vue/define-macros-order': [
'error',
{
order: ['defineProps', 'defineEmits'],
},
],
},
},
prettier,
];
Node.js/Express Configuration
# Install Node-specific plugins
npm install --save-dev eslint-plugin-n eslint-plugin-security
// eslint.config.js - Node.js Configuration
import js from '@eslint/js';
import typescript from 'typescript-eslint';
import nodePlugin from 'eslint-plugin-n';
import security from 'eslint-plugin-security';
import prettier from 'eslint-plugin-prettier/recommended';
export default typescript.config(
js.configs.recommended,
...typescript.configs.recommended,
// Node.js configuration
{
plugins: {
'n': nodePlugin,
'security': security,
},
languageOptions: {
globals: {
...nodePlugin.configs['recommended-module'].languageOptions.globals,
},
},
rules: {
// Node.js rules
'n/no-missing-import': 'off', // TypeScript handles this
'n/no-unsupported-features/es-syntax': 'off',
'n/no-process-exit': 'error',
'n/no-sync': 'warn',
// Security rules
...security.configs.recommended.rules,
'security/detect-object-injection': 'warn',
'security/detect-non-literal-regexp': 'warn',
'security/detect-possible-timing-attacks': 'warn',
},
},
prettier
);
Editor Integration
VS Code Settings
// .vscode/settings.json - Workspace Settings
{
// Format on save
"editor.formatOnSave": true,
"editor.defaultFormatter": "esbenp.prettier-vscode",
// ESLint auto-fix on save
"editor.codeActionsOnSave": {
"source.fixAll.eslint": "explicit",
"source.organizeImports": "never"
},
// Enable ESLint for all supported languages
"eslint.validate": [
"javascript",
"javascriptreact",
"typescript",
"typescriptreact",
"vue",
"html",
"json"
],
// Use flat config
"eslint.useFlatConfig": true,
// File associations
"[javascript]": {
"editor.defaultFormatter": "esbenp.prettier-vscode"
},
"[typescript]": {
"editor.defaultFormatter": "esbenp.prettier-vscode"
},
"[json]": {
"editor.defaultFormatter": "esbenp.prettier-vscode"
},
"[jsonc]": {
"editor.defaultFormatter": "esbenp.prettier-vscode"
},
// TypeScript settings
"typescript.preferences.importModuleSpecifier": "relative",
"typescript.suggest.autoImports": true
}
// .vscode/extensions.json - Recommended Extensions
{
"recommendations": [
"esbenp.prettier-vscode",
"dbaeumer.vscode-eslint",
"streetsidesoftware.code-spell-checker",
"usernamehw.errorlens"
]
}
WebStorm/IntelliJ Settings
Settings → Languages & Frameworks → JavaScript → Prettier
- Enable "On save"
- Run for files: {**/*,*}.{js,ts,jsx,tsx,vue,json,css,scss,md}
Settings → Languages & Frameworks → JavaScript → Code Quality Tools → ESLint
- Enable "Automatic ESLint configuration"
- Enable "Run eslint --fix on save"
Git Hooks with Husky and lint-staged
# Install Husky and lint-staged
npm install --save-dev husky lint-staged
# Initialize Husky
npx husky init
// package.json - lint-staged configuration
{
"lint-staged": {
"*.{js,jsx,ts,tsx}": [
"eslint --fix",
"prettier --write"
],
"*.{json,md,yaml,yml}": [
"prettier --write"
],
"*.{css,scss}": [
"prettier --write"
]
}
}
# .husky/pre-commit
#!/usr/bin/env sh
. "$(dirname -- "$0")/_/husky.sh"
npx lint-staged
# .husky/commit-msg - Optional commit message linting
#!/usr/bin/env sh
. "$(dirname -- "$0")/_/husky.sh"
npx --no -- commitlint --edit $1
CI/CD Integration
# .github/workflows/lint.yml - GitHub Actions
name: Lint and Format
on:
push:
branches: [main, develop]
pull_request:
branches: [main, develop]
jobs:
lint:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: '20'
cache: 'npm'
- name: Install dependencies
run: npm ci
- name: Run TypeScript check
run: npm run typecheck
- name: Run ESLint
run: npm run lint
- name: Check Prettier formatting
run: npm run format:check
- name: Run tests
run: npm test
# GitLab CI equivalent
lint:
stage: validate
image: node:20
cache:
paths:
- node_modules/
script:
- npm ci
- npm run typecheck
- npm run lint
- npm run format:check
rules:
- if: $CI_PIPELINE_SOURCE == 'merge_request_event'
- if: $CI_COMMIT_BRANCH == 'main'
Common Mistakes to Avoid
1. ESLint and Prettier Rule Conflicts
// WRONG - Conflicting formatting rules
export default [
{
rules: {
'indent': ['error', 2], // Conflicts with Prettier
'quotes': ['error', 'single'], // Conflicts with Prettier
'semi': ['error', 'always'], // Conflicts with Prettier
},
},
];
// CORRECT - Let Prettier handle formatting
import prettier from 'eslint-plugin-prettier/recommended';
export default [
{
rules: {
// Only code quality rules, no formatting
'no-unused-vars': 'warn',
'prefer-const': 'error',
},
},
prettier, // Disables conflicting rules and adds prettier/prettier
];
2. Not Using Type-Aware Linting
// WRONG - Missing TypeScript project reference
export default [
...typescript.configs.recommended,
];
// CORRECT - Enable type-aware rules
export default typescript.config(
...typescript.configs.recommendedTypeChecked,
{
languageOptions: {
parserOptions: {
project: true,
tsconfigRootDir: import.meta.dirname,
},
},
}
);
3. Ignoring Auto-Fixable Issues
# WRONG - Manually fixing formatting issues
git add .
git commit -m "fix formatting"
# CORRECT - Let tools auto-fix before commit
npm run lint:fix && npm run format
git add .
git commit -m "feat: add new feature"
4. Different Configs Across Environments
// WRONG - Different settings in CI vs local
// CI might pass while local fails
// CORRECT - Single source of truth
// All settings in eslint.config.js and prettier.config.js
// Same configs used locally, in CI, and in editors
Best Practices Summary
- Use flat config: Migrate to ESLint’s new flat config format for simpler configuration.
- Prettier last: Always add eslint-config-prettier last to disable conflicting rules.
- Automate everything: Use lint-staged and Husky to enforce standards on every commit.
- CI validation: Run lint and format checks in CI to catch issues before merge.
- Editor integration: Configure VS Code or WebStorm for real-time feedback.
- Framework plugins: Use appropriate plugins for React, Vue, or Node.js projects.
- Type-aware rules: Enable TypeScript type-checking rules for deeper analysis.
- Consistent team setup: Share .vscode/settings.json and extensions.json with your team.
Final Thoughts
Combining Prettier and ESLint gives your TypeScript and JavaScript projects both consistency and quality. You’ll save time in code reviews, reduce merge conflicts, and write cleaner, more maintainable code. The modern flat config format makes setup simpler than ever, and tight editor integration ensures developers get immediate feedback.
Once configured with git hooks and CI/CD pipelines, this setup requires minimal maintenance while providing massive productivity benefits. To take it further, read Using Git Hooks to Automate Code Quality Checks and Advanced TypeScript Types and Generics. For official documentation, check the Prettier configuration guide and ESLint flat config documentation.
1 Comment