Linting and Formatting¶
Navigation: Home > Development > Linting and Formatting
Overview¶
This guide covers the code quality tools used in the Simon Stijnen Portfolio project: ESLint for linting and Prettier for formatting. These tools ensure consistent code style and catch potential bugs before they reach production.
Tools Overview¶
ESLint¶
Purpose: Static code analysis to find and fix problems in JavaScript/TypeScript code.
What it catches:
- Syntax errors
- Potential bugs
- Code style violations
- React best practices violations
- TypeScript type issues
- Accessibility problems
Configuration: eslint.config.mjs
Prettier¶
Purpose: Opinionated code formatter that enforces consistent style.
What it formats:
- JavaScript/TypeScript
- JSX/TSX
- JSON
- CSS/SCSS
- Markdown
- And more...
Configuration: .prettierrc.json
Integration¶
ESLint and Prettier work together:
- ESLint checks code quality and logic
- Prettier handles code formatting
eslint-config-prettierdisables conflicting ESLint rules- Both run automatically on commit via Husky
ESLint Configuration¶
Configuration File¶
File: /workspaces/website/eslint.config.mjs
import { dirname } from "path";
import { fileURLToPath } from "url";
import { FlatCompat } from "@eslint/eslintrc";
const __filename = fileURLToPath(import.meta.url);
const __dirname = dirname(__filename);
const compat = new FlatCompat({
baseDirectory: __dirname,
});
const eslintConfig = [
...compat.extends("next/core-web-vitals", "next/typescript"),
...compat.extends("prettier"),
];
export default eslintConfig;
Extends Configuration¶
The project uses ESLint's flat config format with compatibility layer for plugins.
Extended configs:
next/core-web-vitals- Next.js recommended rules
- React rules
- Core Web Vitals rules
-
Performance best practices
-
next/typescript - TypeScript-specific rules
- Type safety checks
-
Import/export validation
-
prettier(eslint-config-prettier) - Disables ESLint formatting rules
- Prevents conflicts with Prettier
- Lets Prettier handle all formatting
Rules from Next.js Config¶
next/core-web-vitals includes:
// React Rules
"react/react-in-jsx-scope": "off" // React 19 doesn't need import React
"react/prop-types": "off" // Use TypeScript instead
"react-hooks/rules-of-hooks": "error" // Enforce hooks rules
"react-hooks/exhaustive-deps": "warn" // Warn on missing dependencies
// Next.js Rules
"@next/next/no-html-link-for-pages": "error" // Use next/link
"@next/next/no-img-element": "warn" // Use next/image
"@next/next/no-sync-scripts": "error" // Async scripts only
// Core Web Vitals
"@next/next/no-before-interactive-script-outside-document": "error"
"@next/next/inline-script-id": "error"
next/typescript includes:
"@typescript-eslint/no-unused-vars": "warn" // Warn on unused variables
"@typescript-eslint/no-explicit-any": "warn" // Warn on 'any' type
"@typescript-eslint/no-non-null-assertion": "warn" // Warn on '!' operator
Running ESLint¶
Basic usage:
# Lint all files
npm run lint
# Output (no errors):
✔ No ESLint warnings or errors
# Output (with errors):
./app/page.tsx
15:7 Error: 'x' is defined but never used @typescript-eslint/no-unused-vars
23:14 Warning: Missing 'key' prop for element in iterator react/jsx-key
✖ 2 problems (1 error, 1 warning)
Auto-fix issues:
Lint specific files:
# Lint single file
npm run lint -- app/page.tsx
# Lint directory
npm run lint -- app/projects/
# Lint pattern
npm run lint -- "app/**/*.tsx"
Advanced options:
# Show detailed errors
npm run lint -- --format=verbose
# Lint and cache results (faster subsequent runs)
npm run lint -- --cache
# Ignore warnings, only show errors
npm run lint -- --quiet
# Maximum warnings before exit with error
npm run lint -- --max-warnings=0
Common ESLint Errors¶
Unused Variables¶
Error:
Solutions:
// Option 1: Remove unused variable
// (delete the line)
// Option 2: Use the variable
const usedVar = 5;
console.log(usedVar);
// Option 3: Prefix with underscore to indicate intentional
const _intentionallyUnused = 5;
Missing Key Prop¶
Error:
Solution:
Using
Instead of ¶
Warning:
<img src="/image.png" alt="Description" />
// Warning: Do not use <img>. Use Image from 'next/image' instead.
Solution:
import Image from "next/image";
<Image src="/image.png" alt="Description" width={500} height={300} />;
Using Instead of ¶
Error:
<a href="/about">About</a>
// Error: Do not use an `<a>` element to navigate to `/about`. Use `<Link />` from `next/link` instead.
Solution:
Prettier Configuration¶
Configuration File¶
File: /workspaces/website/.prettierrc.json
{
"semi": true,
"singleQuote": false,
"tabWidth": 2,
"trailingComma": "es5",
"printWidth": 100,
"plugins": ["prettier-plugin-tailwindcss"]
}
Configuration Options¶
semi: true¶
Add semicolons at end of statements.
singleQuote: false¶
Use double quotes for strings.
tabWidth: 2¶
Use 2 spaces for indentation.
trailingComma: "es5"¶
Add trailing commas where valid in ES5 (objects, arrays).
// Formatted:
const obj = {
name: "Simon",
age: 25, // Trailing comma
};
const arr = [
"item1",
"item2", // Trailing comma
];
// No trailing comma in function parameters (not ES5):
function example(a, b) {}
printWidth: 100¶
Wrap lines longer than 100 characters.
// Formatted (wraps at 100 characters):
const longString =
"This is a very long string that exceeds the print width and will be wrapped to the next line";
plugins: ["prettier-plugin-tailwindcss"]¶
Automatically sort Tailwind CSS classes.
Before:
After:
Sort order:
- Layout (display, position, etc.)
- Box model (margin, padding, etc.)
- Typography (font, text, etc.)
- Visual (color, background, etc.)
- Misc (transitions, animations, etc.)
Running Prettier¶
Format all files:
npm run format
# Output:
app/page.tsx 142ms
app/layout.tsx 89ms
components/project-card.tsx 67ms
lib/config.ts 45ms
Check formatting without changes:
npm run format:check
# Output (formatted):
Checking formatting...
All matched files use Prettier code style!
# Output (not formatted):
Checking formatting...
[warn] app/page.tsx
[warn] components/project-card.tsx
[error] Code style issues found in 2 files. Run Prettier to fix.
Format specific files:
# Format single file
npx prettier --write app/page.tsx
# Format directory
npx prettier --write app/projects/
# Format pattern
npx prettier --write "app/**/*.tsx"
Check single file:
Prettier vs ESLint¶
What each tool does:
Workflow:
- Prettier formats code (spacing, semicolons, etc.)
- ESLint checks code quality (logic, bugs, etc.)
- Both run automatically on commit
Tailwind CSS Class Sorting¶
How It Works¶
The prettier-plugin-tailwindcss plugin automatically sorts Tailwind classes in a consistent order.
Benefits:
- Consistent class order across codebase
- Easier to scan and read classes
- Automatic on save (with IDE integration)
Sort Order Examples¶
Layout first:
// Before:
<div className="flex text-lg items-center">
// After:
<div className="flex items-center text-lg">
Box model before typography:
// Before:
<div className="text-xl p-4 m-2 font-bold">
// After:
<div className="m-2 p-4 text-xl font-bold">
States and variants last:
// Before:
<button className="hover:bg-blue-600 bg-blue-500 p-2 text-white">
// After:
<button className="bg-blue-500 p-2 text-white hover:bg-blue-600">
Complex example:
// Before:
<div className="hover:shadow-lg transition-shadow duration-200 text-lg font-semibold bg-card dark:bg-card-dark p-6 rounded-lg shadow-md mb-4">
// After:
<div className="mb-4 rounded-lg bg-card p-6 text-lg font-semibold shadow-md transition-shadow duration-200 hover:shadow-lg dark:bg-card-dark">
Custom Class Names¶
Non-Tailwind classes are preserved:
// Before:
<div className="custom-class flex items-center another-class">
// After (Tailwind sorted, custom classes preserved):
<div className="custom-class flex items-center another-class">
IDE Integration¶
VSCode / Cursor¶
Install extensions:
- ESLint (
dbaeumer.vscode-eslint) - Prettier (
esbenp.prettier-vscode)
Settings:
{
// Format on save
"editor.formatOnSave": true,
// Use Prettier as default formatter
"editor.defaultFormatter": "esbenp.prettier-vscode",
// Auto-fix ESLint on save
"editor.codeActionsOnSave": {
"source.fixAll.eslint": true
},
// ESLint validation
"eslint.validate": ["javascript", "javascriptreact", "typescript", "typescriptreact"]
}
Workflow:
- Edit file
- Save (Cmd/Ctrl + S)
- Prettier formats automatically
- ESLint fixes auto-fixable issues
- Remaining ESLint errors shown as warnings
Other Editors¶
WebStorm / IntelliJ:
- Prettier: Settings → Prettier → Run on save
- ESLint: Settings → ESLint → Automatic ESLint configuration
Vim / Neovim:
" .vimrc / init.vim
Plug 'prettier/vim-prettier'
Plug 'dense-analysis/ale'
let g:ale_fixers = {'javascript': ['prettier', 'eslint']}
let g:ale_fix_on_save = 1
Git Integration¶
Pre-commit Hook¶
File: .husky/pre-commit
What happens on commit:
- Husky triggers pre-commit hook
- lint-staged runs on staged files only
- Prettier formats changed files
- ESLint lints and auto-fixes
- If errors remain, commit is blocked
lint-staged Configuration¶
File: package.json
{
"lint-staged": {
"*.{js,jsx,ts,tsx}": ["prettier --write", "eslint --fix"],
"*.{json,css,md}": ["prettier --write"]
}
}
What it does:
- JavaScript/TypeScript files:
- Format with Prettier
- Lint with ESLint (with auto-fix)
- Other files (JSON, CSS, Markdown):
- Format with Prettier only
Example commit:
git add app/page.tsx components/button.tsx
git commit -m "feat: update homepage"
# Output:
✔ Preparing lint-staged...
✔ Running tasks for staged files...
✔ package.json — 2 files
✔ *.{js,jsx,ts,tsx} — 2 files
✔ prettier --write
✔ eslint --fix
✔ Applying modifications from tasks...
✔ Cleaning up temporary files...
[main abc1234] feat: update homepage
2 files changed, 42 insertions(+), 12 deletions(-)
If errors exist:
git commit -m "feat: broken code"
# Output:
✖ Running tasks for staged files...
✖ *.{js,jsx,ts,tsx} — 1 file
✖ eslint --fix
app/page.tsx
15:7 error 'x' is defined but never used @typescript-eslint/no-unused-vars
✖ lint-staged failed. Fix errors and try again.
Fix errors and commit again:
# Fix the error in app/page.tsx
git add app/page.tsx
git commit -m "feat: update homepage"
# ✅ Succeeds
Bypassing Hooks¶
Not recommended, but sometimes necessary:
# Skip all git hooks
git commit --no-verify -m "Emergency fix"
# Or
git commit -n -m "Emergency fix"
When to bypass:
- Emergency production fixes
- Work in progress commits (feature branch)
- Known issues being fixed in follow-up commit
Best practice: Fix the issues instead of bypassing.
CI/CD Integration¶
GitHub Actions¶
Example workflow:
# .github/workflows/lint.yml
name: Lint
on: [push, pull_request]
jobs:
lint:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: actions/setup-node@v3
with:
node-version: "20"
- name: Install dependencies
run: npm ci
- name: Run ESLint
run: npm run lint
- name: Check Prettier formatting
run: npm run format:check
What it does:
- Runs on every push and PR
- Installs dependencies
- Runs ESLint (fails if errors)
- Checks Prettier formatting (fails if not formatted)
CI Check Command¶
Run all checks locally:
Use before pushing:
# Check everything before pushing
npm run ci:check
git push
# Or combine:
npm run ci:check && git push
Ignoring Files¶
.eslintignore¶
File: .eslintignore (optional, not in project by default)
# Dependencies
node_modules/
# Build output
.next/
out/
build/
# Cache
.cache/
coverage/
# Environment
.env
.env.local
Default ignored (even without file):
node_modules/.next/out/
.prettierignore¶
File: .prettierignore (optional, not in project by default)
# Dependencies
node_modules/
# Build output
.next/
out/
build/
# Logs
*.log
# Lock files
package-lock.json
yarn.lock
pnpm-lock.yaml
Custom Rules¶
Adding ESLint Rules¶
Edit eslint.config.mjs:
const eslintConfig = [
...compat.extends("next/core-web-vitals", "next/typescript"),
...compat.extends("prettier"),
{
rules: {
// Custom rules
"no-console": ["warn", { allow: ["warn", "error"] }], // Warn on console.log
"@typescript-eslint/no-unused-vars": ["error", { argsIgnorePattern: "^_" }], // Ignore _vars
"react/self-closing-comp": "error", // Enforce <Component />
},
},
];
Example custom rules:
{
rules: {
// Disallow console.log in production
"no-console": process.env.NODE_ENV === "production" ? "error" : "warn",
// Enforce consistent import order
"import/order": [
"error",
{
groups: ["builtin", "external", "internal", "parent", "sibling", "index"],
"newlines-between": "always",
},
],
// Enforce button type attribute
"react/button-has-type": "error",
// Prevent missing props validation (if not using TypeScript)
"react/prop-types": "off", // We use TypeScript
// Maximum line length (handled by Prettier)
"max-len": "off",
}
}
Disabling Rules¶
Disable for entire file:
Disable for single line:
Disable specific rules:
Best practice: Use sparingly, prefer fixing the issue.
Troubleshooting¶
Issue: ESLint Not Running¶
Check:
Restart IDE (VSCode/Cursor) to reload ESLint extension.
Issue: Prettier Not Formatting¶
Check:
Check IDE settings:
- Format on save enabled
- Prettier set as default formatter
Issue: Conflicting Rules¶
Error:
Cause: Line ending conflicts (Windows vs Unix)
Solution:
Or add to .prettierrc.json:
Issue: Husky Hooks Not Running¶
Check:
# Verify Husky is installed
npm list husky
# Reinstall hooks
npm run prepare
chmod +x .husky/pre-commit
Best Practices¶
✅ Do's¶
- Run
npm run ci:checkbefore pushing - Fix ESLint errors, don't disable them
- Use auto-fix (
--fix) for quick fixes - Configure IDE to format on save
- Commit frequently with pre-commit hooks enabled
❌ Don'ts¶
- Don't disable ESLint rules without good reason
- Don't commit without running checks
- Don't bypass git hooks regularly
- Don't ignore warnings (they often indicate problems)
- Don't mix formatting styles (let Prettier handle it)
See Also¶
- Development Workflow - Using linting in daily workflow
- NPM Scripts - Lint and format commands
- Git Hooks - Automated linting on commit
- Testing - Linting test files
- ESLint Documentation
- Prettier Documentation
Next Steps¶
- Set up Git hooks for automatic linting
- Configure your IDE with linting extensions
- Run tests to ensure code quality
Last updated: February 2026