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 preferences
  • singleQuote: true - Cleaner in JavaScript/TypeScript
  • semi: false - Less visual noise, modern JS
  • printWidth: 120 - Modern screens allow longer lines
  • trailingComma: 'es5' - Facilitates Git diffs
  • arrowParens: '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 support
  • eslint-plugin-react-hooks - Rules for React hooks
  • eslint-plugin-prettier - Prettier integration in ESLint
  • eslint-plugin-perfectionist - Automatic sorting (imports, objects...)
  • @tanstack/eslint-plugin-query - Rules for TanStack Query
  • eslint-plugin-jsx-a11y - JSX accessibility
  • eslint-plugin-promise - Promise best practices
  • eslint-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"
  ]
}
  1. 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
    
  2. Configuration of files (.prettierrc.js, eslint.config.js)

  3. Scripts in package.json to automate

  4. IDE configuration for automatic formatting

  5. Pre-commit hooks (optional)

    npm install -D husky lint-staged
    

Resources for Further Learning