Linters and Formatters
Linters and formatters improve code quality by finding errors, bugs, and style issues (linters) and automatically correcting formatting such as indentation and spacing (formatters). They ensure consistency, readability, and early error detection.
Examples: ESLint, Prettier, Biome, Rome...
Why Use Linters and Formatters?
Linters (ESLint, TSLint...)
- Error detection - Unused variables, missing imports
- Adherence to best practices - Naming conventions, recommended patterns
- Security - Detection of potential vulnerabilities
- Team consistency - Same code style for everyone
Formatters (Prettier, Biome...)
- Automatic formatting - Indentation, spaces, line breaks
- Time saving - No need to format manually
- Avoids debates - Uniform style defined once and for all
- Readability - Cleaner and easier-to-read code
Prettier Configuration (Formatter)
Here is my personal Prettier configuration that prioritizes readability and consistency (for example):
.prettierrc.js
module.exports = {
// Use tabs instead of spaces
useTabs: true,
tabWidth: 2,
// Trailing commas (ES5 compatible)
trailingComma: 'es5',
// Single quotes for JS/TS
singleQuote: true,
jsxSingleQuote: false, // Double quotes for JSX
// No semicolons
semi: false,
// Maximum line width
printWidth: 120,
// Spacing in objects { foo: bar }
bracketSpacing: true,
// Parentheses for arrow functions
arrowParens: 'avoid', // x => x instead of (x) => x
// Object properties
quoteProps: 'as-needed', // Quotes only when necessary
// Line break management
proseWrap: 'preserve',
endOfLine: 'auto',
// Formatting of embedded languages
embeddedLanguageFormatting: 'auto',
htmlWhitespaceSensitivity: 'css',
// Pragmas (special comments)
requirePragma: false,
insertPragma: false,
// Plugin for Tailwind CSS (class sorting)
plugins: ['prettier-plugin-tailwindcss'],
}
Explanation of choices
useTabs: true
- Tabs adapt to everyone's indentation preferencessingleQuote: true
- Cleaner in JavaScript/TypeScriptsemi: false
- Less visual noise, modern JSprintWidth: 120
- Modern screens allow longer linestrailingComma: 'es5'
- Facilitates Git diffsarrowParens: 'avoid'
- More concise for single-parameter functions
ESLint Configuration (Linter)
Modern ESLint configuration with TypeScript and React:
eslint.config.js
import reactHooksPlugin from 'eslint-plugin-react-hooks'
import eslintPluginPrettierRecommended from 'eslint-plugin-prettier/recommended'
import noOnlyTestsPlugin from 'eslint-plugin-no-only-tests'
import queryPlugin from '@tanstack/eslint-plugin-query'
import perfectionist from 'eslint-plugin-perfectionist'
import tsPlugin from '@typescript-eslint/eslint-plugin'
import jsxA11yPlugin from 'eslint-plugin-jsx-a11y'
import promisePlugin from 'eslint-plugin-promise'
import tsParser from '@typescript-eslint/parser'
import { FlatCompat } from '@eslint/eslintrc'
import { fileURLToPath } from 'url'
import * as espree from 'espree'
import { dirname } from 'path'
const __filename = fileURLToPath(import.meta.url)
const __dirname = dirname(__filename)
const compat = new FlatCompat({
baseDirectory: __dirname,
})
// Common rule configuration
const baseRules = {
// React Hooks - Disabled because sometimes too strict
'react-hooks/exhaustive-deps': 'off',
// Promises - Not always necessary to return
'promise/always-return': 'off',
// Prettier integration
'prettier/prettier': 'error',
// Tests - Prevents .only() in production
'no-only-tests/no-only-tests': 'error',
// Console - Allows warn, error, info, debug
'no-console': ['error', { allow: ['warn', 'error', 'info', 'debug'] }],
// Accessibility - Disabled because sometimes too strict
'jsx-a11y/anchor-has-content': 'off',
'jsx-a11y/alt-text': 'off',
// Next.js - Allows native img tags
'@next/next/no-img-element': 'off',
}
// Configuration of the Perfectionist plugin (automatic sorting)
const perfectionistRules = {
// Sorting object properties
'perfectionist/sort-objects': [
'warn',
{
type: 'natural',
order: 'desc',
},
],
// Sorting imports (very useful!)
'perfectionist/sort-imports': [
'error',
{
type: 'line-length',
order: 'desc',
newlinesBetween: 'always',
// Internal project patterns
internalPattern: [
'@/app/.*',
'@/components/.*',
'@/lib/.*',
'@/models/.*',
'@/services/.*',
'@/constants/.*'
],
// Order of import groups
groups: [
'type',
'react',
'nanostores',
['builtin', 'external'],
'internal-type',
'internal',
['parent-type', 'sibling-type', 'index-type'],
['parent', 'sibling', 'index'],
'side-effect',
'style',
'object',
'unknown',
],
// Custom groups
customGroups: {
value: {
react: ['react', 'react-*'],
nanostores: '@nanostores/.*',
},
type: {
react: 'react',
},
},
},
],
// Sorting enums
'perfectionist/sort-enums': [
'error',
{
type: 'natural',
order: 'desc',
},
],
}
// Common plugins
const basePlugins = {
reactHooks: reactHooksPlugin,
perfectionist,
'no-only-tests': noOnlyTestsPlugin,
jsxA11y: jsxA11yPlugin,
}
// Parser options
const baseParserOptions = {
sourceType: 'module',
ecmaVersion: 'latest',
ecmaFeatures: { jsx: true },
}
const eslintConfig = [
// Next.js extensions
...compat.extends('next/core-web-vitals', 'next/typescript'),
// Files to ignore
{
ignores: [
'node_modules/**',
'.next/**',
'out/**',
'build/**',
'next-env.d.ts'
],
},
// Recommended configurations
eslintPluginPrettierRecommended,
...queryPlugin.configs['flat/recommended'],
promisePlugin.configs['flat/recommended'],
// Configuration for JavaScript
{
rules: {
...baseRules,
...perfectionistRules,
},
plugins: basePlugins,
languageOptions: {
parserOptions: baseParserOptions,
parser: espree,
},
files: ['**/*.{js,jsx,mjs,cjs}'],
},
// Configuration for TypeScript
{
rules: {
...baseRules,
...perfectionistRules,
// Specific TypeScript rules
...tsPlugin.configs.recommended.rules,
...tsPlugin.configs['recommended-type-checked'].rules,
// TypeScript strict
'@typescript-eslint/strict-boolean-expressions': 'error',
'@typescript-eslint/prefer-optional-chain': 'error',
'@typescript-eslint/prefer-nullish-coalescing': 'error',
'@typescript-eslint/no-unused-vars': 'error',
'@typescript-eslint/no-unsafe-member-access': 'error',
'@typescript-eslint/no-unsafe-assignment': 'error',
'@typescript-eslint/no-unsafe-argument': 'error',
'@typescript-eslint/no-unnecessary-type-assertion': 'error',
'@typescript-eslint/no-explicit-any': 'error',
},
plugins: {
'@typescript-eslint': tsPlugin,
...basePlugins,
},
languageOptions: {
parserOptions: {
...baseParserOptions,
project: './tsconfig.json',
},
parser: tsParser,
},
files: ['**/*.{ts,tsx}'],
},
]
export default eslintConfig
Plugins Used
🔧 Essential ESLint Plugins
@typescript-eslint
- Complete TypeScript supporteslint-plugin-react-hooks
- Rules for React hookseslint-plugin-prettier
- Prettier integration in ESLinteslint-plugin-perfectionist
- Automatic sorting (imports, objects...)@tanstack/eslint-plugin-query
- Rules for TanStack Queryeslint-plugin-jsx-a11y
- JSX accessibilityeslint-plugin-promise
- Promise best practiceseslint-plugin-no-only-tests
- Avoids .only() tests
🎨 Prettier Plugin
prettier-plugin-tailwindcss
- Automatic Tailwind class sorting
package.json Scripts
{
"scripts": {
"lint": "eslint . --ext .js,.jsx,.ts,.tsx",
"lint:fix": "eslint . --ext .js,.jsx,.ts,.tsx --fix",
"format": "prettier --write .",
"format:check": "prettier --check .",
"code:check": "npm run lint && npm run format:check",
"code:fix": "npm run lint:fix && npm run format"
}
}
IDE Configuration
VS Code (.vscode/settings.json
)
{
"editor.formatOnSave": true,
"editor.defaultFormatter": "esbenp.prettier-vscode",
"editor.codeActionsOnSave": {
"source.fixAll.eslint": true,
"source.organizeImports": true
},
"eslint.validate": [
"javascript",
"javascriptreact",
"typescript",
"typescriptreact"
]
}
Recommended Workflow
-
Installation
npm install -D eslint prettier npm install -D @typescript-eslint/parser @typescript-eslint/eslint-plugin npm install -D eslint-plugin-prettier eslint-config-prettier
-
Configuration of files (
.prettierrc.js
,eslint.config.js
) -
Scripts in package.json to automate
-
IDE configuration for automatic formatting
-
Pre-commit hooks (optional)
npm install -D husky lint-staged