Why Linting and Formatting Matter
Consistent code style and automated quality checks are foundational to maintainable codebases. ESLint catches bugs, enforces best practices, and prevents anti-patterns. Prettier handles formatting so developers never argue about tabs vs spaces, semicolons, or line length. Together, they create a safety net that catches issues before code reaches code review.
ESLint vs Prettier
- ESLint: Analyzes code for quality issues (unused variables, missing dependencies in hooks, type errors, accessibility violations). Can fix some issues automatically.
- Prettier: Reformats code for consistent style (indentation, line breaks, quotes, semicolons). Has no opinions about code quality - only formatting.
- Together: Prettier handles formatting, ESLint handles quality. Use
eslint-config-prettierto prevent conflicts between them.
ESLint Flat Config (eslint.config.js)
ESLint 9+ uses the new "flat config" format. This is a single JavaScript file that exports an array of configuration objects. It replaces the old .eslintrc format.
// eslint.config.js (ESLint 9+ flat config)
import js from '@eslint/js';
import tseslint from 'typescript-eslint';
import react from 'eslint-plugin-react';
import reactHooks from 'eslint-plugin-react-hooks';
import jsxA11y from 'eslint-plugin-jsx-a11y';
import importPlugin from 'eslint-plugin-import';
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
{
plugins: {
react,
'react-hooks': reactHooks,
'jsx-a11y': jsxA11y,
},
rules: {
...react.configs.recommended.rules,
...reactHooks.configs.recommended.rules,
...jsxA11y.configs.recommended.rules,
'react/react-in-jsx-scope': 'off',
'react/prop-types': 'off',
},
settings: {
react: { version: 'detect' },
},
},
// Import ordering
{
plugins: { import: importPlugin },
rules: {
'import/order': ['error', {
groups: [
'builtin', 'external', 'internal', 'parent', 'sibling', 'index'
],
'newlines-between': 'always',
alphabetize: { order: 'asc', caseInsensitive: true },
}],
'import/no-duplicates': 'error',
},
},
// Custom rules
{
rules: {
'@typescript-eslint/no-unused-vars': ['error', {
argsIgnorePattern: '^_',
varsIgnorePattern: '^_',
}],
'@typescript-eslint/no-explicit-any': 'warn',
'@typescript-eslint/consistent-type-imports': ['error', {
prefer: 'type-imports',
}],
'no-console': ['warn', { allow: ['warn', 'error'] }],
},
},
// Disable formatting rules (Prettier handles formatting)
prettier,
// Ignore patterns
{
ignores: [
'node_modules/**',
'.next/**',
'dist/**',
'coverage/**',
'*.config.js',
],
}
);
Prettier Configuration
// .prettierrc
{
"semi": true,
"singleQuote": true,
"tabWidth": 2,
"trailingComma": "all",
"printWidth": 100,
"bracketSpacing": true,
"arrowParens": "always",
"endOfLine": "lf",
"jsxSingleQuote": false,
"plugins": ["prettier-plugin-tailwindcss"],
"tailwindConfig": "./tailwind.config.ts",
"overrides": [
{
"files": "*.md",
"options": {
"printWidth": 80,
"proseWrap": "always"
}
}
]
}
# .prettierignore
node_modules
.next
dist
coverage
pnpm-lock.yaml
package-lock.json
Running Linters
# ESLint commands
npx eslint . # Lint all files
npx eslint . --fix # Auto-fix fixable issues
npx eslint src/components/ # Lint specific directory
npx eslint --cache # Cache results for faster re-runs
npx eslint --debug # Show which config/rules are applied
# Prettier commands
npx prettier --check . # Check formatting (CI-friendly)
npx prettier --write . # Format all files
npx prettier --write "src/**/*.tsx" # Format specific files
# Install dependencies
npm install -D eslint prettier typescript-eslint \
eslint-config-prettier eslint-plugin-react \
eslint-plugin-react-hooks eslint-plugin-jsx-a11y \
eslint-plugin-import prettier-plugin-tailwindcss
VS Code Integration
// .vscode/settings.json
{
"editor.defaultFormatter": "esbenp.prettier-vscode",
"editor.formatOnSave": true,
"editor.codeActionsOnSave": {
"source.fixAll.eslint": "explicit",
"source.organizeImports": "never"
},
"eslint.validate": [
"javascript",
"javascriptreact",
"typescript",
"typescriptreact"
]
}
EditorConfig
# .editorconfig - Editor-agnostic formatting basics
root = true
[*]
indent_style = space
indent_size = 2
end_of_line = lf
charset = utf-8
trim_trailing_whitespace = true
insert_final_newline = true
[*.md]
trim_trailing_whitespace = false
[Makefile]
indent_style = tab
Linting Best Practices
- Fix, do not disable: When ESLint flags an issue, fix the code. Only add disable comments for genuine false positives, and always add a justification comment.
- Use eslint-config-prettier: Always include this to prevent ESLint from conflicting with Prettier formatting rules.
- Enable type-aware linting: TypeScript-ESLint with type checking catches bugs that regular ESLint cannot (like unchecked promises and type safety issues).
- Run in CI: Always run
eslintandprettier --checkin your CI pipeline to catch issues before merge. - Start strict, relax later: Begin with strict configurations and disable specific rules only when the team agrees.