ในบทความนี้
In this post
- 🤔 Why Does Code Quality Matter?
- 🧹 Linting & Formatting — Making Tidy Code Mandatory
- 🔍 Code Review — The Art of Reviewing Code
- 🧠 SOLID Principles — The Rules of Good Code
- 🚨 Code Smells — When Code Starts to Stink
- 📊 Static Analysis — Let the Machine Inspect It
- 🛠️ The Full Toolkit
- 📋 CI/CD Quality Pipeline
- 📝 Clean Code Checklist
- Summary
จากโพสต์ที่แล้ว เราเรียน GitHub Actions ที่ช่วย automate CI/CD — แต่ pipeline ดีแค่ไหน ถ้า code ข้างในห่วยก็ไม่ช่วย!
วันนี้มาเรียนรู้ Code Quality — ทำยังไงให้ code อ่านง่าย, maintain ได้, bug น้อย และทีมทำงานร่วมกันได้ดี 🔍🐕
🤔 ทำไม Code Quality สำคัญ?
ลองนึกภาพว่าคุณต้องกลับมาแก้ code ที่เขียนเมื่อ 6 เดือนก่อน — ถ้า code อ่านไม่รู้เรื่อง ไม่มี test ไม่มี documentation คุณจะเสียเวลาทำความเข้าใจมากกว่าเวลาแก้จริง! Code Quality ไม่ใช่ "เรื่องสวยงาม" แต่คือ ต้นทุนจริง — code คุณภาพต่ำ = bug เยอะ + แก้ช้า + deploy กลัว + developer ใหม่สับสน
| Code ดี 😊 | Code แย่ 😰 |
|---|---|
| อ่านแล้วเข้าใจใน 5 นาที | อ่าน 1 ชั่วโมงยังงง |
| แก้ bug ง่าย → แก้จุดเดียว | แก้ 1 จุด พัง 10 จุด |
| คนใหม่ onboard ได้เร็ว | คนเดิมลาออก = ตาย |
| Deploy มั่นใจ | Deploy แล้วสวดมนต์ |
| Technical debt ต่ำ | Technical debt สะสมจนล้มละลาย |
💡 "Any fool can write code that a computer can understand. Good programmers write code that humans can understand." — Martin Fowler
🧹 Linting & Formatting — บังคับให้เขียนสวย
Linting คือการใช้เครื่องมือตรวจสอบ code อัตโนมัติเพื่อหา bugs, bad practices, และ style issues ส่วน Formatting คือการจัด code ให้สวยเหมือนกันทั้งทีม (indentation, quotes, semicolons) — ไม่ต้องเถียงกันอีกว่า tab หรือ space! เมื่อ setup แล้ว ทุกคนในทีมจะเขียน code style เดียวกันอัตโนมัติ
ESLint (JavaScript/TypeScript)
ESLint เป็น linter ยอดนิยมที่สุดสำหรับ JavaScript/TypeScript — สามารถ customize rules ได้ตามความต้องการของทีม ตั้งแต่ห้ามใช้ any type ไปจนถึงบังคับ naming conventions ตัวอย่างด้านล่างแสดง config ที่แนะนำ:
# ติดตั้ง
npm install --save-dev eslint @eslint/js typescript-eslint
# eslint.config.js
import js from '@eslint/js';
import tseslint from 'typescript-eslint';
export default [
js.configs.recommended,
...tseslint.configs.recommended,
{
rules: {
'no-console': 'warn', // console.log = warning
'no-unused-vars': 'error', // ตัวแปรไม่ใช้ = error
'no-var': 'error', // ห้ามใช้ var
'prefer-const': 'error', // ใช้ const ถ้าไม่ reassign
'eqeqeq': 'error', // ห้าม == ต้อง ===
'@typescript-eslint/no-explicit-any': 'warn',
}
}
];
# รัน
npx eslint . # ตรวจ
npx eslint . --fix # ตรวจ + แก้อัตโนมัติ
Prettier (Auto-format)
Prettier ทำงานร่วมกับ ESLint — ESLint ดูแล "ถูก-ผิด" ส่วน Prettier ดูแล "สวย-ไม่สวย" แค่ตั้งค่าครั้งเดียว Prettier จะ format code ให้สวยทุกครั้งที่ save — ไม่ต้องเสียเวลาจัด whitespace เอง:
# ติดตั้ง
npm install --save-dev prettier
# .prettierrc
{
"semi": true,
"singleQuote": true,
"tabWidth": 2,
"trailingComma": "es5",
"printWidth": 80
}
# รัน
npx prettier --write . # format ทุกไฟล์
npx prettier --check . # ตรวจ (ใน CI)
Python — Ruff (เร็วที่สุด!)
Ruff เป็น Python linter + formatter ตัวใหม่ที่เขียนด้วย Rust — เร็วกว่า flake8 + isort + black รวมกัน 10-100 เท่า! แทนที่จะต้องตั้งค่า 3 tools แยกกัน Ruff ทำได้ทุกอย่างในตัวเดียว:
# ติดตั้ง
pip install ruff
# pyproject.toml
[tool.ruff]
line-length = 88
select = ["E", "F", "W", "I", "N", "UP"]
ignore = ["E501"]
[tool.ruff.format]
quote-style = "double"
# รัน
ruff check . # lint
ruff check . --fix # lint + fix
ruff format . # format (แทน black)
Pre-commit Hooks — บังคับก่อน commit
ปัญหาของ linting/formatting คือ developer อาจลืมรัน! Pre-commit hooks แก้ปัญหานี้โดยบังคับให้ lint + format อัตโนมัติก่อนทุก commit — ถ้า code ไม่ผ่าน commit จะ fail ไม่ต้องพึ่ง "วินัย" อีกต่อไป เพราะระบบบังคับให้:
# ติดตั้ง
npm install --save-dev husky lint-staged
npx husky init
# .husky/pre-commit
npx lint-staged
# package.json
{
"lint-staged": {
"*.{js,ts,tsx}": [
"eslint --fix",
"prettier --write"
],
"*.{json,md,yml}": [
"prettier --write"
]
}
}
# ตอนนี้ทุกครั้งที่ git commit → auto lint + format!
# ❌ code ไม่ผ่าน lint = commit ไม่ได้
🔍 Code Review — ศิลปะการ review code
เครื่องมืออัตโนมัติจับ bugs ได้ แต่ไม่สามารถบอกได้ว่า "approach นี้ถูกต้องไหม?" หรือ "มี design ที่ดีกว่านี้ไหม?" นั่นคือหน้าที่ของ Code Review — กระบวนการที่ developer คนอื่นอ่านและให้ feedback กับ code ของเรา ไม่ใช่แค่หา bugs แต่เป็นการแชร์ความรู้ สร้างมาตรฐาน และป้องกันปัญหาที่เครื่องมือมองไม่เห็น
Code Review Flow
กระบวนการ Code Review ที่ดีควรมี flow ที่ชัดเจน — ตั้งแต่ developer เปิด PR ไปจนถึงการ merge เข้า main branch สิ่งสำคัญคือต้อง review ภายใน 24 ชั่วโมง เพื่อไม่ให้ block workflow ของทีม:
Reviewer — ดูอะไรบ้าง?
Reviewer ที่ดีไม่ใช่แค่ดู syntax — แต่ต้องดูทั้ง correctness, design, readability, performance, security และ test coverage หลักการสำคัญคือ "Could a new team member understand this code?" ถ้าตอบว่าไม่ แสดงว่า code ต้องปรับปรุง:
| หมวด | ดูอะไร | ตัวอย่าง |
|---|---|---|
| 🐛 Correctness | ทำงานถูกมั้ย | Edge cases, off-by-one, null checks |
| 📖 Readability | อ่านรู้เรื่องมั้ย | ชื่อตัวแปร, comments, structure |
| 🏗️ Design | ออกแบบดีมั้ย | SOLID, DRY, separation of concerns |
| ⚡ Performance | เร็วพอมั้ย | N+1 queries, unnecessary loops |
| 🔒 Security | ปลอดภัยมั้ย | SQL injection, XSS, hardcoded secrets |
| 🧪 Tests | มี test ครอบคลุมมั้ย | Happy path + edge cases + error cases |
ตัวอย่าง Review Comment ที่ดี
Review comment ที่ดีต้อง อธิบาย "ทำไม" ไม่ใช่แค่บอกว่า "แก้ตรงนี้" — เพื่อให้ developer เรียนรู้และไม่ทำผิดซ้ำ ควรแนะนำวิธีแก้ พร้อม code ตัวอย่าง และใช้ tone ที่ constructive ไม่ใช่ criticize:
// ✅ Review comment ที่ดี: // "This could throw if `user` is null. // Consider adding a null check: // `if (!user) return res.status(404).json(...)`"
// ✅ อีกตัวอย่าง: // "Nit: prefer `const` here since `items` // is never reassigned."
🐕 Review Culture: Review code ไม่ใช่ review คน — "code นี้น่าจะ..." ไม่ใช่ "คุณเขียนแย่..." | ใช้ "Nit:" สำหรับ nitpick เล็กๆ | ชมเมื่อเห็น code ดี 👍
🧠 SOLID Principles — หลักการ code ที่ดี
SOLID คือ 5 หลักการออกแบบ software ที่ช่วยให้ code flexible, maintainable, และ extensible คิดค้นโดย Robert C. Martin (Uncle Bob) แม้ชื่อจะดูน่ากลัว แต่แก่นแท้คือ — แยกส่วนให้ชัดเจน, แต่ละส่วนทำหน้าที่เดียว, เปลี่ยนแปลงได้ง่าย มาดูแต่ละตัว:
ตัวอย่าง: Single Responsibility
Single Responsibility Principle (SRP) บอกว่า "แต่ละ class/function ควรมีเหตุผลในการเปลี่ยนแปลงแค่อันเดียว" ถ้า function ทำทั้ง validate, save, send email → แก้ email logic อาจพัง save logic! ตัวอย่างด้านล่างเปรียบเทียบ code ที่ละเมิด SRP กับ code ที่ทำตาม:
// ✅ ดี: แยก responsibility ชัดเจน + class OrderService { + constructor(validator, payment, notifier, inventory) { + this.validator = validator; + this.payment = payment; + this.notifier = notifier; + this.inventory = inventory; + } + async process(order) { + this.validator.validate(order); + await this.payment.charge(order); + await this.inventory.update(order); + await this.notifier.send(order); + } + }
🚨 Code Smells — กลิ่นของ code ที่ต้องแก้
Code Smells คือ "สัญญาณ" ว่า code อาจมีปัญหา — ไม่ใช่ bugs โดยตรง แต่เป็น patterns ที่บ่งบอกว่า design ไม่ดี ถ้าปล่อยไว้จะกลายเป็น technical debt ที่แก้ยากขึ้นเรื่อยๆ เหมือน "กลิ่น" — ไม่ได้ทำร้ายทันที แต่บอกว่ามีอะไรเน่าอยู่!
| Code Smell | ปัญหา | วิธีแก้ |
|---|---|---|
| 🔢 Magic Numbers | if (status === 3) | ใช้ constant: ORDER_STATUS.COMPLETED |
| 📏 Long Function | Function 200+ บรรทัด | แยกเป็น functions เล็กๆ |
| 📦 God Class | Class ทำทุกอย่าง | แยกตาม responsibility (SRP) |
| 🔄 Duplicated Code | Copy-paste เหมือนกัน | Extract เป็น shared function |
| 🏗️ Deep Nesting | if ซ้อน 5+ ชั้น | Early return, guard clauses |
| 📝 Dead Code | Code ที่ไม่ถูกเรียกใช้ | ลบทิ้ง (Git เก็บ history ให้) |
| 🔗 Feature Envy | ใช้ data ของ class อื่นเยอะ | ย้าย method ไป class นั้น |
| 💬 Bad Naming | let x, tmp, data2 | ชื่อบอกความหมาย |
ตัวอย่าง: Deep Nesting → Early Return
หนึ่งใน code smells ที่พบบ่อยที่สุดคือ Deep Nesting — if ซ้อน if ซ้อน if จนอ่านไม่รู้เรื่อง วิธีแก้คือ Early Return Pattern — เช็ค error cases ก่อนแล้ว return ออกเลย ทำให้ "happy path" อยู่ด้านนอกสุดและอ่านง่าย:
// ✅ ดี: Early return (flat) + function getDiscount(user) { + if (!user) return 0; + if (!user.isPremium) return 0; + if (user.orders > 10) return 0.2; + return 0.1; + }
📊 Static Analysis — ให้เครื่องตรวจ
นอกจาก linter แล้ว ยังมี Static Analysis tools ที่วิเคราะห์ code เชิงลึกกว่า — หา code smells อัตโนมัติ, วัด complexity, ตรวจจับ security vulnerabilities, และ track technical debt ตลอดเวลา เครื่องมือเหล่านี้เป็นเหมือน "ตาที่สาม" ที่ช่วย reviewer
SonarQube / SonarCloud
SonarQube (self-hosted) หรือ SonarCloud (managed) เป็น platform วิเคราะห์ code ที่ครบเครื่องที่สุด — ใช้กับ GitHub Actions ได้ สแกนทุก PR และแสดงผลลัพธ์เป็น dashboard พร้อม metrics ต่างๆ:
# GitHub Actions + SonarCloud
name: Quality Gate
on: [push, pull_request]
jobs:
sonarcloud:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0 # full history for blame
- name: SonarCloud Scan
uses: SonarSource/sonarcloud-github-action@master
env:
SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }}
with:
args: >
-Dsonar.projectKey=myorg_myapp
-Dsonar.organization=myorg
-Dsonar.sources=src
-Dsonar.tests=tests
-Dsonar.javascript.lcov.reportPaths=coverage/lcov.info
SonarQube วัดอะไรบ้าง?
SonarQube วัด code quality ใน 5 มิติหลัก — Bugs, Vulnerabilities, Code Smells, Coverage, และ Duplications แต่ละมิติมีเกรด (A-E) ที่ช่วยให้เห็นภาพรวมได้เร็ว:
| Metric | วัดอะไร | เป้าหมาย |
|---|---|---|
| 🐛 Bugs | Code ที่น่าจะผิด | 0 (A rating) |
| 🔓 Vulnerabilities | Security issues | 0 (A rating) |
| 💩 Code Smells | Maintainability issues | น้อยที่สุด |
| 📋 Coverage | Test coverage % | > 80% |
| 🔄 Duplications | Duplicated code % | < 3% |
| 🏗️ Complexity | Cyclomatic complexity | < 15 per function |
| 📏 Debt | Technical debt เวลาแก้ | Trend ลดลง |
Quality Gate — ผ่านหรือไม่ผ่าน
Quality Gate คือ "ด่าน" ที่กำหนดว่า code ต้องผ่านเกณฑ์ขั้นต่ำก่อน merge ได้ — เช่น coverage ต้อง > 80%, ห้ามมี Critical bugs, duplications < 3% ถ้าไม่ผ่าน PR จะ fail อัตโนมัติ:
# Quality Gate Conditions (SonarQube default)
# ──────────────────────────────────────────
# ✅ PASS ถ้า:
# - New code coverage > 80%
# - New code duplications < 3%
# - New bugs = 0 (A rating)
# - New vulnerabilities = 0 (A rating)
# - New code smells ≤ A rating
# - New security hotspots reviewed 100%
#
# ❌ FAIL → PR ไม่สามารถ merge ได้!
# → บังคับคุณภาพอัตโนมัติ ไม่ต้องพึ่ง reviewer
🛠️ Tools รวม — ครบชุด
สรุปเครื่องมือทั้งหมดที่ใช้ในการรักษา Code Quality — แบ่งตาม category เพื่อให้เลือกใช้ได้ง่าย ไม่จำเป็นต้องใช้ทุกตัว เริ่มจาก linter + formatter ก่อน แล้วค่อยเพิ่มตามความพร้อม:
| หมวด | JavaScript/TS | Python | General |
|---|---|---|---|
| Linter | ESLint, Biome | Ruff, Pylint, Flake8 | — |
| Formatter | Prettier, Biome | Ruff format, Black | — |
| Type Check | TypeScript | mypy, Pyright | — |
| Static Analysis | SonarCloud | SonarCloud, Bandit | SonarQube |
| Complexity | ESLint rules | radon, xenon | CodeClimate |
| Dead Code | ts-prune, knip | vulture | — |
| Pre-commit | Husky + lint-staged | pre-commit | — |
| AI Review | GitHub Copilot, CodeRabbit, Sourcery | ||
📋 CI/CD Quality Pipeline
ถึงเวลารวมทุกเครื่องมือเข้าด้วยกันใน GitHub Actions! Pipeline ด้านล่างแสดง quality checks ที่ควรรันทุก PR เรียงจากเร็วไปช้า — format check (วินาที) → lint (วินาที) → type check (นาที) → tests + coverage (นาที) → SonarCloud (นาที):
# .github/workflows/quality.yml
name: Code Quality
on: [push, pull_request]
jobs:
quality:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 20
cache: 'npm'
- run: npm ci
# Step 1: Format check
- name: Check formatting
run: npx prettier --check .
# Step 2: Lint
- name: Lint
run: npx eslint .
# Step 3: Type check
- name: Type check
run: npx tsc --noEmit
# Step 4: Tests + coverage
- name: Tests
run: npm test -- --coverage
# Step 5: Coverage threshold
- name: Check coverage threshold
run: |
COVERAGE=$(jq '.total.lines.pct' coverage/coverage-summary.json)
echo "Coverage: $COVERAGE%"
if (( $(echo "$COVERAGE < 80" | bc -l) )); then
echo "❌ Coverage below 80%!"
exit 1
fi
# Step 6: SonarCloud
- uses: SonarSource/sonarcloud-github-action@master
env:
SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }}
Pipeline ข้างต้นจะแสดงการเรียงลำดับขั้นตอนการตรวจสอบจากเร็วไปช้า และจากพื้นฐานไปซับซ้อน การเริ่มด้วย formatting check จะให้ feedback เร็วที่สุดหาก code ไม่เป็นไปตามมาตรฐาน ตามด้วย linting ที่จะตรวจสอบ syntax และ basic rules
Type checking จะช่วยจับข้อผิดพลาดเรื่อง type safety ซึ่งสำคัญมากใน TypeScript การรัน tests พร้อมกับ coverage จะยืนยันว่า functionality ยังทำงานถูกต้อง และมี test coverage ที่เพียงพอ ขั้นตอนสุดท้ายคือ SonarCloud ที่จะทำการวิเคราะห์เชิงลึกและรวบรวม metrics ทั้งหมด
📝 Clean Code Checklist
หลังจากที่เราได้เรียนรู้เครื่องมือและกระบวนการต่างๆ มาแล้ว สิ่งสำคัญคือการนำหลักการและแนวปฏิบัติเหล่านี้มาใช้ในการเขียน code ในชีวิตประจำวัน Clean Code Checklist ต่อไปนี้จะเป็นแนวทางที่นักพัฒนาสามารถใช้เป็น guideline ในการเขียนและ review code
Checklist นี้เป็นการรวบรวมหลักการสำคัญที่ทำให้ code มีคุณภาพ โดยแต่ละข้อจะเน้นในด้านต่างๆ ตั้งแต่การตั้งชื่อ, การออกแบบ function, ไปจนถึงกระบวนการ development ที่ดี การปฏิบัติตาม checklist นี้จะช่วยยกระดับคุณภาพ code อย่างมีระบบ:
- Naming: ชื่อตัวแปร/function บอกความหมาย —
getUserByIdไม่ใช่getData - Functions: สั้น ทำอย่างเดียว ไม่เกิน 20 บรรทัด
- DRY: Don't Repeat Yourself — extract shared logic
- KISS: Keep It Simple, Stupid — อย่า over-engineer
- Comments: อธิบาย "ทำไม" ไม่ใช่ "อะไร" — code บอก what, comment บอก why
- Error Handling: handle errors explicitly ไม่ใช่ catch แล้วเงียบ
- No Magic: ใช้ constants แทน magic numbers/strings
- Consistent: ทั้ง codebase ใช้ style เดียวกัน (enforce ด้วย linter)
- Tests: ทุก function สำคัญต้องมี test
- Small PRs: PR เล็กๆ review ง่าย — ไม่เกิน 400 lines changed
Clean Code Checklist ข้างต้นครอบคลุมหลักการสำคัญทุกด้านของการเขียน code ที่มีคุณภาพ ตั้งแต่ technical aspects อย่าง naming conventions และ error handling ไปจนถึง process aspects อย่าง code review และการจัดการ PR ขนาดเล็ก การปฏิบัติตามทุกข้ออาจดูเยอะ แต่เมื่อกลายเป็นนิสัยแล้วจะทำได้โดยอัตโนมัติ
💡 กฎ Boy Scout: "Leave the code better than you found it" — เจอ code smell ก็แก้ทีละนิด ไม่ต้องรอ refactor ใหญ่
กฎ Boy Scout เป็นหลักการง่ายๆ แต่มีพลังมาก การแก้ไข code smell เล็กๆ น้อยๆ ทีละนิดจะช่วยป้องกันไม่ให้ technical debt สะสมจนกลายเป็นปัญหาใหญ่ แนวทางนี้ยั่งยืนกว่าการรอให้มีปัญหาใหญ่แล้วค่อย refactor ทั้งหมด และยังช่วยให้ทีมมีความรู้สึก ownership กับ codebase มากขึ้น
สรุป
Code Quality เป็นเรื่องที่ต้องทำอย่างต่อเนื่องและเป็นระบบ ไม่ใช่เรื่องที่ทำครั้งเดียวแล้วเสร็จ การใช้เครื่องมือต่างๆ ที่กล่าวมาในบทความนี้จะช่วยให้การสร้างและรักษาคุณภาพ code เป็นไปอย่างอัตโนมัติและมีประสิทธิภาพมากขึ้น
สิ่งสำคัญที่สุดคือการสร้าง culture ที่ให้ความสำคัญกับคุณภาพ code ในทีม เครื่องมือต่างๆ เป็นเพียงตัวช่วย แต่ใจสำคัญอยู่ที่ mindset และวัฒนธรรมของทีมที่ต้องการเขียน code ที่ดี ช่วยเหลือกัน และเรียนรู้จากกัน นี่คือจุดเริ่มต้นของ sustainable software development:
- Linting + Formatting = ESLint + Prettier (JS) / Ruff (Python) บังคับ style ด้วยเครื่อง
- Pre-commit hooks = Husky + lint-staged → commit ไม่ได้ถ้า code ไม่ผ่าน
- Code Review = ดู correctness, readability, design, security, tests
- SOLID Principles = 5 หลักการออกแบบ OOP ที่ทำให้ code maintain ง่าย
- Code Smells = magic numbers, deep nesting, god class → fix ด้วย early return, extract function
- Static Analysis = SonarQube/SonarCloud วัด bugs, vulnerabilities, coverage, debt
- Quality Gates = PR merge ไม่ได้ถ้าไม่ผ่าน quality threshold
- Clean Code = good naming, small functions, DRY, KISS, Boy Scout Rule 🔍🐕
การนำแนวปฏิบัติเหล่านี้มาใช้ไม่จำเป็นต้องทำทุกอย่างพร้อมกัน สามารถเริ่มจากเครื่องมือพื้นฐานอย่าง linter และ formatter ก่อน แล้วค่อยๆ เพิ่ม static analysis, code review process และ quality gates ตามความพร้อมของทีม สิ่งสำคัญคือความสม่ำเสมอและการปรับปรุงอย่างต่อเนื่อง เพราะ code quality ไม่ใช่จุดหมาย แต่เป็นการเดินทางที่ไม่มีวันสิ้นสุด 🔍🐕
In the previous post we looked at GitHub Actions and how it automates CI/CD — but however good the pipeline is, it cannot save bad code inside it!
Today we look at Code Quality — how to make code readable, maintainable, low on bugs, and pleasant for a team to work on together 🔍🐕
🤔 Why Does Code Quality Matter?
Picture yourself coming back to fix code you wrote six months ago — if it is unreadable, has no tests and no documentation, you will spend more time understanding it than actually fixing it! Code quality is not a matter of "prettiness"; it is a real cost — low-quality code means more bugs, slower fixes, deploys you dread, and new developers left confused.
| Good code 😊 | Bad code 😰 |
|---|---|
| Understood in five minutes | An hour of reading and still lost |
| Easy bug fixes → one place to change | Fix one thing, break ten others |
| New people onboard quickly | The one who knows it leaves = disaster |
| Deploy with confidence | Deploy and pray |
| Low technical debt | Technical debt piles up until you are bankrupt |
💡 "Any fool can write code that a computer can understand. Good programmers write code that humans can understand." — Martin Fowler
🧹 Linting & Formatting — Making Tidy Code Mandatory
Linting means using tools that inspect code automatically to find bugs, bad practices and style issues, while Formatting means laying code out identically across the whole team (indentation, quotes, semicolons) — no more arguing about tabs versus spaces! Once it is set up, everyone on the team writes in the same style automatically.
ESLint (JavaScript/TypeScript)
ESLint is the most popular linter for JavaScript and TypeScript — its rules can be customized to whatever the team needs, from banning the any type to enforcing naming conventions. The example below shows a config worth starting from:
# install
npm install --save-dev eslint @eslint/js typescript-eslint
# eslint.config.js
import js from '@eslint/js';
import tseslint from 'typescript-eslint';
export default [
js.configs.recommended,
...tseslint.configs.recommended,
{
rules: {
'no-console': 'warn', // console.log = warning
'no-unused-vars': 'error', // unused variable = error
'no-var': 'error', // never use var
'prefer-const': 'error', // use const when never reassigned
'eqeqeq': 'error', // no ==, always ===
'@typescript-eslint/no-explicit-any': 'warn',
}
}
];
# run
npx eslint . # check
npx eslint . --fix # check + fix automatically
Prettier (Auto-format)
Prettier works alongside ESLint — ESLint looks after "right or wrong", Prettier looks after "tidy or untidy". Configure it once and Prettier formats your code on every save — no more hand-arranging whitespace:
# install
npm install --save-dev prettier
# .prettierrc
{
"semi": true,
"singleQuote": true,
"tabWidth": 2,
"trailingComma": "es5",
"printWidth": 80
}
# run
npx prettier --write . # format every file
npx prettier --check . # check (in CI)
Python — Ruff (The Fastest!)
Ruff is a new Python linter and formatter written in Rust — 10 to 100 times faster than flake8 + isort + black combined! Instead of configuring three separate tools, Ruff does all of it in one:
# install
pip install ruff
# pyproject.toml
[tool.ruff]
line-length = 88
select = ["E", "F", "W", "I", "N", "UP"]
ignore = ["E501"]
[tool.ruff.format]
quote-style = "double"
# run
ruff check . # lint
ruff check . --fix # lint + fix
ruff format . # format (replaces black)
Pre-commit Hooks — Enforced Before the Commit
The trouble with linting and formatting is that a developer can forget to run them! Pre-commit hooks solve that by running lint and format automatically before every commit — if the code does not pass, the commit fails. No more relying on "discipline", because the system enforces it:
# install
npm install --save-dev husky lint-staged
npx husky init
# .husky/pre-commit
npx lint-staged
# package.json
{
"lint-staged": {
"*.{js,ts,tsx}": [
"eslint --fix",
"prettier --write"
],
"*.{json,md,yml}": [
"prettier --write"
]
}
}
# now every git commit → auto lint + format!
# ❌ code that fails lint = no commit
🔍 Code Review — The Art of Reviewing Code
Automated tools catch bugs, but they cannot tell you "is this approach correct?" or "is there a better design here?". That is the job of Code Review — the process where another developer reads your code and gives feedback. It is not only about finding bugs; it spreads knowledge, sets standards, and catches the problems no tool can see.
Code Review Flow
A good code review process needs a clear flow — from the developer opening a PR all the way to the merge into the main branch. The important part is reviewing within 24 hours, so the team's workflow is never blocked:
What Should a Reviewer Look At?
A good reviewer does not just read syntax — they look at correctness, design, readability, performance, security and test coverage. The guiding question is "Could a new team member understand this code?" If the answer is no, the code needs work:
| Category | What to look for | Examples |
|---|---|---|
| 🐛 Correctness | Does it actually work? | Edge cases, off-by-one, null checks |
| 📖 Readability | Can it be understood? | Variable names, comments, structure |
| 🏗️ Design | Is it well designed? | SOLID, DRY, separation of concerns |
| ⚡ Performance | Is it fast enough? | N+1 queries, unnecessary loops |
| 🔒 Security | Is it safe? | SQL injection, XSS, hardcoded secrets |
| 🧪 Tests | Is it covered by tests? | Happy path + edge cases + error cases |
What a Good Review Comment Looks Like
A good review comment explains "why" instead of simply saying "change this" — so the developer learns and does not repeat the mistake. Suggest the fix, show sample code, and keep the tone constructive rather than critical:
// ✅ A good review comment: // "This could throw if `user` is null. // Consider adding a null check: // `if (!user) return res.status(404).json(...)`"
// ✅ Another example: // "Nit: prefer `const` here since `items` // is never reassigned."
🐕 Review Culture: Review the code, not the person — "this code could…" rather than "you wrote this badly…" | Use "Nit:" for small nitpicks | Praise good code when you see it 👍
🧠 SOLID Principles — The Rules of Good Code
SOLID is a set of five software design principles that keep code flexible, maintainable and extensible, formulated by Robert C. Martin (Uncle Bob). The names sound intimidating, but the core is simple — separate concerns clearly, give each part one job, and make change easy. Here they are one by one:
Example: Single Responsibility
The Single Responsibility Principle (SRP) says that each class or function should have exactly one reason to change. If a function validates, saves and sends email all at once, touching the email logic can break the save logic! The example below compares code that violates SRP with code that follows it:
// ✅ Good: responsibilities cleanly separated + class OrderService { + constructor(validator, payment, notifier, inventory) { + this.validator = validator; + this.payment = payment; + this.notifier = notifier; + this.inventory = inventory; + } + async process(order) { + this.validator.validate(order); + await this.payment.charge(order); + await this.inventory.update(order); + await this.notifier.send(order); + } + }
🚨 Code Smells — When Code Starts to Stink
Code smells are "signals" that code may be in trouble — not bugs in themselves, but patterns that point to a poor design. Left alone they turn into technical debt that gets harder to pay off. They are like a smell: nothing hurts you immediately, but something in there is rotting!
| Code Smell | Problem | Fix |
|---|---|---|
| 🔢 Magic Numbers | if (status === 3) | Use a constant: ORDER_STATUS.COMPLETED |
| 📏 Long Function | A function of 200+ lines | Split it into small functions |
| 📦 God Class | A class that does everything | Split it by responsibility (SRP) |
| 🔄 Duplicated Code | Identical copy-paste | Extract a shared function |
| 🏗️ Deep Nesting | if nested 5+ levels deep | Early return, guard clauses |
| 📝 Dead Code | Code that is never called | Delete it (Git keeps the history) |
| 🔗 Feature Envy | Heavy use of another class's data | Move the method into that class |
| 💬 Bad Naming | let x, tmp, data2 | Names that carry meaning |
Example: Deep Nesting → Early Return
One of the most common code smells is deep nesting — if inside if inside if until nobody can read it. The fix is the early return pattern: check the error cases first and return immediately, which leaves the "happy path" at the outermost level and easy to read:
// ✅ Good: early return (flat) + function getDiscount(user) { + if (!user) return 0; + if (!user.isPremium) return 0; + if (user.orders > 10) return 0.2; + return 0.1; + }
📊 Static Analysis — Let the Machine Inspect It
Beyond linters there are static analysis tools that examine code far more deeply — finding code smells automatically, measuring complexity, detecting security vulnerabilities, and tracking technical debt over time. Think of them as a "third eye" helping the reviewer.
SonarQube / SonarCloud
SonarQube (self-hosted) or SonarCloud (managed) is the most complete code-analysis platform there is — it plugs into GitHub Actions, scans every PR, and reports the results on a dashboard full of metrics:
# GitHub Actions + SonarCloud
name: Quality Gate
on: [push, pull_request]
jobs:
sonarcloud:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0 # full history for blame
- name: SonarCloud Scan
uses: SonarSource/sonarcloud-github-action@master
env:
SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }}
with:
args: >
-Dsonar.projectKey=myorg_myapp
-Dsonar.organization=myorg
-Dsonar.sources=src
-Dsonar.tests=tests
-Dsonar.javascript.lcov.reportPaths=coverage/lcov.info
What Does SonarQube Measure?
SonarQube measures code quality across five main dimensions — bugs, vulnerabilities, code smells, coverage and duplications. Each carries a grade from A to E, which makes the overall picture quick to read:
| Metric | What it measures | Target |
|---|---|---|
| 🐛 Bugs | Code that is probably wrong | 0 (A rating) |
| 🔓 Vulnerabilities | Security issues | 0 (A rating) |
| 💩 Code Smells | Maintainability issues | As few as possible |
| 📋 Coverage | Test coverage % | > 80% |
| 🔄 Duplications | Duplicated code % | < 3% |
| 🏗️ Complexity | Cyclomatic complexity | < 15 per function |
| 📏 Debt | Technical debt, as time to fix | A falling trend |
Quality Gate — Pass or Fail
A quality gate is the checkpoint that defines the minimum a change must meet before it can be merged — coverage above 80%, no critical bugs, duplications under 3%. Fail it and the PR fails automatically:
# Quality Gate Conditions (SonarQube default)
# ──────────────────────────────────────────
# ✅ PASS if:
# - New code coverage > 80%
# - New code duplications < 3%
# - New bugs = 0 (A rating)
# - New vulnerabilities = 0 (A rating)
# - New code smells ≤ A rating
# - New security hotspots reviewed 100%
#
# ❌ FAIL → the PR cannot be merged!
# → quality enforced automatically, no need to rely on the reviewer
🛠️ The Full Toolkit
Here is every tool used to keep code quality up, grouped by category so it is easy to choose from. You do not need all of them — start with a linter and a formatter, then add more as the team is ready:
| Category | JavaScript/TS | Python | General |
|---|---|---|---|
| Linter | ESLint, Biome | Ruff, Pylint, Flake8 | — |
| Formatter | Prettier, Biome | Ruff format, Black | — |
| Type Check | TypeScript | mypy, Pyright | — |
| Static Analysis | SonarCloud | SonarCloud, Bandit | SonarQube |
| Complexity | ESLint rules | radon, xenon | CodeClimate |
| Dead Code | ts-prune, knip | vulture | — |
| Pre-commit | Husky + lint-staged | pre-commit | — |
| AI Review | GitHub Copilot, CodeRabbit, Sourcery | ||
📋 CI/CD Quality Pipeline
Time to bring every tool together in GitHub Actions! The pipeline below shows the quality checks worth running on every PR, ordered fastest to slowest — format check (seconds) → lint (seconds) → type check (a minute) → tests + coverage (minutes) → SonarCloud (minutes):
# .github/workflows/quality.yml
name: Code Quality
on: [push, pull_request]
jobs:
quality:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 20
cache: 'npm'
- run: npm ci
# Step 1: Format check
- name: Check formatting
run: npx prettier --check .
# Step 2: Lint
- name: Lint
run: npx eslint .
# Step 3: Type check
- name: Type check
run: npx tsc --noEmit
# Step 4: Tests + coverage
- name: Tests
run: npm test -- --coverage
# Step 5: Coverage threshold
- name: Check coverage threshold
run: |
COVERAGE=$(jq '.total.lines.pct' coverage/coverage-summary.json)
echo "Coverage: $COVERAGE%"
if (( $(echo "$COVERAGE < 80" | bc -l) )); then
echo "❌ Coverage below 80%!"
exit 1
fi
# Step 6: SonarCloud
- uses: SonarSource/sonarcloud-github-action@master
env:
SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }}
The pipeline above orders its checks from fast to slow, and from basic to sophisticated. Starting with the formatting check gives the fastest possible feedback when code does not meet the standard, followed by linting, which catches syntax and the basic rules.
Type checking then catches type-safety mistakes, which matters enormously in TypeScript. Running the tests together with coverage confirms that the functionality still works and that test coverage is adequate. The final step is SonarCloud, which performs the deep analysis and gathers all of the metrics together.
📝 Clean Code Checklist
Having gone through the tools and the processes, the thing that matters is putting these principles and practices to work in the code you write every day. The Clean Code Checklist below is a guideline developers can use both when writing and when reviewing code.
The checklist gathers the principles that make code good, each one covering a different angle — from naming, to function design, to sound development process. Following it raises code quality in a systematic way:
- Naming: variable and function names carry meaning —
getUserById, notgetData - Functions: short, doing one thing, no more than 20 lines
- DRY: Don't Repeat Yourself — extract shared logic
- KISS: Keep It Simple, Stupid — do not over-engineer
- Comments: explain "why", not "what" — the code says what, the comment says why
- Error Handling: handle errors explicitly, never catch and swallow silently
- No Magic: use constants instead of magic numbers and strings
- Consistent: one style across the whole codebase (enforced by a linter)
- Tests: every important function has a test
- Small PRs: small PRs are easy to review — no more than 400 lines changed
The checklist covers every important aspect of quality code, from technical matters such as naming conventions and error handling through to process matters such as code review and keeping PRs small. Following all of it may look like a lot, but once it becomes habit it happens automatically.
💡 The Boy Scout Rule: "Leave the code better than you found it" — when you meet a code smell, fix it a little at a time; there is no need to wait for a big refactor.
The Boy Scout Rule is simple but powerful. Fixing small code smells a little at a time stops technical debt from accumulating into a serious problem. It is a more sustainable approach than waiting for the problem to grow and then refactoring everything at once, and it gives the team a stronger sense of ownership over the codebase.
Summary
Code quality is something you work at continuously and systematically, not something you do once and finish. The tools covered in this post make building and preserving that quality automatic, and far more effective.
The most important thing is building a team culture that values code quality. Tools are only aids; the heart of it is a mindset and a team culture that wants to write good code, help one another, and learn from one another. That is where sustainable software development begins:
- Linting + Formatting = ESLint + Prettier (JS) / Ruff (Python), style enforced by machine
- Pre-commit hooks = Husky + lint-staged → no commit if the code does not pass
- Code Review = look at correctness, readability, design, security, tests
- SOLID Principles = five OOP design principles that keep code easy to maintain
- Code Smells = magic numbers, deep nesting, god class → fix with early return, extract function
- Static Analysis = SonarQube/SonarCloud measuring bugs, vulnerabilities, coverage, debt
- Quality Gates = a PR cannot merge unless it passes the quality threshold
- Clean Code = good naming, small functions, DRY, KISS, the Boy Scout Rule 🔍🐕
None of this has to arrive all at once. Start with the basic tools — a linter and a formatter — then add static analysis, a code review process and quality gates as the team is ready. What matters is consistency and continuous improvement, because code quality is not a destination but a journey with no end 🔍🐕