Skip to main content

ESLint + Prettier

Overview​

The application uses ESLint for code quality and consistency, and Prettier for automatic code formatting. Together, they enforce uniform code style across the monorepo.

What Are ESLint and Prettier?​

ESLint​

ESLint is a static code analysis tool that:

  • Detects Problems: Finds bugs, security issues, and problematic patterns
  • Enforces Style: Ensures consistent coding style
  • Customizable: Highly configurable with plugins and rules
  • Automatic Fixes: Can automatically fix many issues

Prettier​

Prettier is an opinionated code formatter that:

  • Consistent Formatting: Auto-formats code consistently
  • Zero Configuration: Works with sensible defaults
  • Opinionated: Makes style choices so you don't have to
  • Language Agnostic: Supports JS, TS, JSX, TSX, JSON, CSS, YAML, etc.

App Configuration​

Root ESLint Configuration​

eslint.config.mjs​

import js from '@eslint/js';
import service from '@typescript-eslint/eslint-plugin';
import parser from '@typescript-eslint/parser';
import eslintConfigPrettier from 'eslint-config-prettier';

export default [
// Ignore node_modules and build directories
{
ignores: ['node_modules', 'dist', 'build', 'coverage', '.next'],
},

// Base JavaScript rules
{
files: ['**/*.js', '**/*.mjs'],
languageOptions: {
ecmaVersion: 2020,
sourceType: 'module',
},
rules: {
...js.configs.recommended.rules,
},
},

// TypeScript rules
{
files: ['**/*.ts', '**/*.tsx'],
languageOptions: {
parser,
parserOptions: {
ecmaVersion: 2020,
sourceType: 'module',
ecmaFeatures: { jsx: true },
},
},
plugins: {
'@typescript-eslint': service,
},
rules: {
...service.configs.recommended.rules,
'@typescript-eslint/explicit-function-return-types': 'warn',
'@typescript-eslint/no-unused-vars': ['error', { argsIgnorePattern: '^_' }],
'@typescript-eslint/no-explicit-any': 'warn',
'@typescript-eslint/explicit-module-boundary-types': 'off',
},
},

// Disable Prettier conflicting rules
eslintConfigPrettier,
];

Prettier Configuration​

.prettierrc.json​

{
"semi": true,
"trailingComma": "es5",
"singleQuote": true,
"printWidth": 100,
"tabWidth": 2,
"useTabs": false,
"bracketSpacing": true,
"arrowParens": "always",
"proseWrap": "preserve",
"endOfLine": "lf"
}

.prettierignore​

node_modules
dist
build
coverage
.next
pnpm-lock.yaml
out
.turbo

Backend (NestJS) ESLint​

apps/api/.eslintrc.js​

module.exports = {
parser: '@typescript-eslint/parser',
parserOptions: {
project: 'tsconfig.json',
sourceType: 'module',
},
plugins: ['@typescript-eslint/eslint-plugin'],
extends: ['plugin:@typescript-eslint/recommended', 'plugin:prettier/recommended'],
root: true,
env: {
node: true,
jest: true,
},
ignorePatterns: ['.eslintrc.js', 'dist', 'coverage'],
rules: {
'@typescript-eslint/interface-name-prefix': 'off',
'@typescript-eslint/explicit-function-return-types': 'warn',
'@typescript-eslint/explicit-module-boundary-types': 'off',
'@typescript-eslint/no-explicit-any': 'off',
'@typescript-eslint/no-unused-vars': ['error', { argsIgnorePattern: '^_' }],
'no-console': ['warn', { allow: ['warn', 'error'] }],
'no-restricted-syntax': [
'error',
{
selector:
'CallExpression[callee.object.name="console"][callee.property.name!=/^(warn|error)$/]',
message: 'Unexpected console method',
},
],
},
};

Frontend (Next.js) ESLint​

apps/web/.eslintrc.json​

{
"extends": ["next", "plugin:@typescript-eslint/recommended", "plugin:prettier/recommended"],
"parser": "@typescript-eslint/parser",
"plugins": ["@typescript-eslint", "react", "react-hooks"],
"rules": {
"react/react-in-jsx-scope": "off",
"react/prop-types": "off",
"react-hooks/rules-of-hooks": "error",
"react-hooks/exhaustive-deps": "warn",
"@typescript-eslint/explicit-function-return-types": "warn",
"@typescript-eslint/no-unused-vars": ["error", { "argsIgnorePattern": "^_" }],
"@next/next/no-html-link-for-pages": "off"
}
}

Running ESLint & Prettier​

ESLint Commands​

# Lint all files
pnpm lint

# Lint specific package
pnpm --filter @qms/api lint

# Lint specific file
pnpm lint src/services/user.service.ts

# Fix auto-fixable issues
pnpm lint -- --fix

# Show only warnings
pnpm lint -- --fix --max-warnings=0

# Generate report
pnpm lint -- --format json > lint-report.json

Prettier Commands​

# Format all files
pnpm format

# Format specific package
pnpm --filter @qms/web format

# Check formatting without modifying
pnpm format -- --check

# Format specific file
pnpm format src/components/Header.tsx

# Format specific file types
pnpm format -- --write "**/*.{ts,tsx,json,md}"

Combined Lint & Format​

# Run both
pnpm lint && pnpm format

# Or in script
pnpm -r run lint:fix && pnpm -r run format

Rules Configuration​

Common ESLint Rules​

{
"rules": {
// Error prevention
"no-console": ["warn", { "allow": ["warn", "error"] }],
"no-debugger": "error",
"no-duplicate-imports": "error",
"no-unused-vars": ["error", { "argsIgnorePattern": "^_" }],

// Best practices
"eqeqeq": ["error", "always"],
"no-implied-eval": "error",
"no-new-func": "error",
"prefer-const": "error",
"prefer-template": "warn",

// TypeScript specific
"@typescript-eslint/explicit-function-return-types": "warn",
"@typescript-eslint/strict-boolean-expressions": "off",
"@typescript-eslint/no-explicit-any": "warn",
}
}

React Specific Rules​

{
"plugins": ["react", "react-hooks"],
"rules": {
"react/prop-types": "off", // Using TypeScript
"react/react-in-jsx-scope": "off", // Not needed in JSX transform
"react-hooks/rules-of-hooks": "error",
"react-hooks/exhaustive-deps": "warn",
"react/no-array-index-key": "warn",
"react/no-unescaped-entities": "warn",
}
}

Integration​

IDE Integration​

VS Code Settings​

Create .vscode/settings.json:

{
"editor.defaultFormatter": "esbenp.prettier-vscode",
"editor.formatOnSave": true,
"[typescript]": {
"editor.defaultFormatter": "esbenp.prettier-vscode",
"editor.formatOnSave": true,
"editor.codeActionsOnSave": {
"source.fixAll.eslint": true
}
},
"[typescriptreact]": {
"editor.defaultFormatter": "esbenp.prettier-vscode",
"editor.formatOnSave": true,
"editor.codeActionsOnSave": {
"source.fixAll.eslint": true
}
},
"eslint.validate": ["javascript", "javascriptreact", "typescript", "typescriptreact"]
}

Git Hooks Integration​

Part of Husky + lint-staged setup (see related documentation):

# Run before commit
pnpm lint --fix
pnpm format

# Prevents bad code from being committed

Common Issues & Solutions​

ESLint vs Prettier Conflicts​

Prettier conflicts are handled with eslint-config-prettier:

// This disables all ESLint rules that conflict with Prettier
import eslintConfigPrettier from 'eslint-config-prettier';

export default [
// ... other configs
eslintConfigPrettier, // Must be last
];

Ignoring Files​

Create .eslintignore:

node_modules
dist
build
coverage
.next
pnpm-lock.yaml

Or in config:

{
ignores: ['node_modules', 'dist', '**/*.min.js'];
}

Fixing Formatting Issues​

# Fix ESLint issues
pnpm lint -- --fix

# Format with Prettier
pnpm format

# Both together
pnpm lint -- --fix && pnpm format

Best Practices​

1. Use Meaningful Rule Configuration​

// βœ… GOOD: Tailored to project
{
"@typescript-eslint/explicit-function-return-types": "warn",
"no-console": ["warn", { "allow": ["warn", "error"] }]
}

// ❌ AVOID: Too strict everywhere
{
"@typescript-eslint/explicit-function-return-types": "error"
}

2. Disable Rules Judiciously​

// βœ… GOOD: Explain why rule is disabled
// eslint-disable-next-line @typescript-eslint/no-explicit-any
// Using any because external library types are incomplete
const externalData: any = await externalLib.getData();

// ❌ AVOID: Disable without explanation
// eslint-disable-next-line
const externalData: any = await externalLib.getData();

3. Consistent Prettier Config​

Use .prettierrc.json for single source of truth:

{
"semi": true,
"singleQuote": true,
"printWidth": 100
}

4. Run Checks Before Commit​

This is handled by Husky + lint-staged:

# Automatically run on commit
git commit -m "feat: add feature"
# β†’ eslint --fix
# β†’ prettier --write
# β†’ commit if passes

Monorepo Considerations​

Root Configuration​

Configure at root for consistency:

# Lint all packages
pnpm -r run lint

# Format all packages
pnpm -r run format

Package-Specific Overrides​

Each package can extend root config:

// apps/api/.eslintrc.js
module.exports = {
extends: '../../.eslintrc.js',
rules: {
'no-console': 'off', // Servers can log
},
};

Scripts in Root package.json​

{
"scripts": {
"lint": "eslint . --ext .ts,.tsx,.js,.jsx",
"lint:fix": "eslint . --ext .ts,.tsx,.js,.jsx --fix",
"format": "prettier --write .",
"format:check": "prettier --check .",
"code:fix": "pnpm lint:fix && pnpm format"
}
}

Troubleshooting​

ESLint Not Finding Config​

# Check if config file exists
ls -la .eslintrc* eslint.config.*

# Verify config syntax
cat .eslintrc.json # Should be valid JSON

Prettier Formatting Conflicts​

# Ensure eslint-config-prettier is installed
pnpm list eslint-config-prettier

# Place it last in extends
module.exports = {
extends: [..., 'prettier'] // Must be last
}

Performance Issues​

# Run with cache
pnpm lint -- --cache

# Check specific file
pnpm lint src/services/user.service.ts

# Profile ESLint
pnpm lint -- --debug

Resources​