Pre-commit hook dengan Husky dan lint-staged membantu menangkap masalah yang paling sering lolos ke pull request: format berantakan, error lint, dan perubahan kecil yang seharusnya bisa diperbaiki otomatis sebelum kode di-push. Dengan menjalankan pemeriksaan pada file yang berubah saja, feedback datang lebih cepat daripada menunggu pipeline CI.

Namun hook lokal bukan pengganti CI. Hook efektif untuk validasi cepat di mesin developer, sementara CI tetap diperlukan sebagai sumber kebenaran akhir karena hook bisa dilewati, lingkungan lokal bisa berbeda, dan beberapa pemeriksaan memang terlalu berat jika dijalankan di setiap commit. Artikel ini fokus pada setup yang praktis, cepat, dan cukup ketat untuk menjaga PR tetap bersih tanpa membuat developer frustrasi.

Masalah yang Diselesaikan oleh Pre-commit Hook

Tanpa pre-commit hook, masalah berikut sering muncul di pull request:

  • Formatting tidak konsisten, misalnya spasi, kutip, trailing comma, atau urutan import.
  • Error lint sederhana yang seharusnya bisa ditemukan sebelum kode dikirim.
  • Perubahan kecil bercampur dengan noise, sehingga review menjadi lebih sulit.
  • Feedback terlambat karena menunggu CI hanya untuk gagal pada aturan dasar.

Dengan Husky dan lint-staged, Anda bisa mengotomatisasi pengecekan saat git commit dijalankan. Husky bertugas memasang hook Git, sedangkan lint-staged menjalankan command hanya pada file yang sedang di-stage. Kombinasi ini cocok untuk menjaga kualitas dasar tanpa membebani seluruh repository pada setiap commit.

Kapan Hook Lokal Efektif, dan Kapan Tidak

Kapan efektif

  • Untuk tugas cepat seperti Prettier, ESLint, atau validasi ringan pada file yang berubah.
  • Pada tim yang ingin mengurangi PR dengan masalah kosmetik.
  • Untuk monorepo atau proyek sedang-besar, selama command dibatasi hanya pada file staged.

Batasannya

  • Bisa di-bypass, misalnya dengan git commit --no-verify.
  • Lingkungan lokal tidak selalu sama dengan CI atau mesin anggota tim lain.
  • Check berat seperti full test suite, build penuh, atau type check seluruh workspace sering terlalu lambat untuk pre-commit.
  • Beberapa validasi membutuhkan konteks lintas file atau hasil build, sehingga lebih cocok dijalankan di pre-push atau CI.

Prinsip yang aman: gunakan pre-commit untuk fast feedback, dan gunakan CI untuk authoritative validation.

Arsitektur Singkat: Husky dan lint-staged Bekerja Bagaimana?

Husky menambahkan script hook Git, misalnya pre-commit. Saat Anda menjalankan git commit, Git akan memanggil hook tersebut.

lint-staged membaca daftar file yang sudah di-stage, lalu mengeksekusi command hanya untuk file yang relevan. Ini penting karena:

  • lebih cepat daripada menjalankan lint ke seluruh codebase,
  • lebih fokus pada perubahan yang benar-benar akan masuk ke commit,
  • lebih mudah diterima tim karena tidak terasa berat.

Jika command mengubah file, lint-staged dapat menambahkan kembali hasil perubahan itu ke staging area, sehingga developer tidak perlu menjalankan langkah tambahan secara manual.

Instalasi Dasar di Proyek JavaScript/TypeScript

Berikut contoh setup umum pada proyek Node.js dengan npm. Jika Anda memakai pnpm atau yarn, sesuaikan perintah instalasinya, tetapi konsepnya tetap sama.

npm install -D husky lint-staged eslint prettier typescript

Jika proyek Anda sudah punya ESLint, Prettier, atau TypeScript, tentu tidak perlu memasangnya lagi.

Selanjutnya aktifkan Husky:

npx husky init

Perintah ini biasanya membuat folder .husky dan file hook awal. Setelah itu, pastikan ada script untuk menyiapkan Husky ketika dependency diinstal. Contoh di package.json:

{
  "scripts": {
    "prepare": "husky"
  }
}

Script prepare berguna agar hook tetap terpasang setelah npm install, termasuk saat clone proyek di mesin baru.

Konfigurasi Pre-commit Hook dengan lint-staged

Opsi 1: Simpan konfigurasi di package.json

Untuk setup sederhana, Anda bisa menaruh konfigurasi langsung di package.json:

{
  "scripts": {
    "prepare": "husky",
    "lint": "eslint .",
    "format": "prettier --write .",
    "typecheck": "tsc --noEmit"
  },
  "lint-staged": {
    "*.{js,jsx,ts,tsx}": [
      "eslint --fix",
      "prettier --write"
    ],
    "*.{json,md,css,scss,yml,yaml}": [
      "prettier --write"
    ]
  }
}

Lalu isi hook .husky/pre-commit agar menjalankan lint-staged:

npx lint-staged

Konfigurasi ini berarti:

  • File JavaScript/TypeScript yang di-stage akan diperiksa dengan ESLint dan diformat dengan Prettier.
  • File seperti JSON, Markdown, CSS, dan YAML hanya diformat dengan Prettier.
  • Hanya file yang berubah yang diproses, bukan seluruh repository.

Opsi 2: Gunakan file terpisah

Jika konfigurasi mulai panjang, file terpisah lebih mudah dirawat. Contoh .lintstagedrc.json:

{
  "*.{js,jsx,ts,tsx}": [
    "eslint --fix",
    "prettier --write"
  ],
  "*.{json,md,css,scss,yml,yaml}": [
    "prettier --write"
  ]
}

Pendekatan ini lebih nyaman untuk tim karena file konfigurasi tidak bercampur dengan script aplikasi di package.json.

Integrasi dengan ESLint, Prettier, dan Type Check Ringan

ESLint untuk error dan auto-fix

ESLint cocok dijalankan di pre-commit karena banyak aturan bisa diperbaiki otomatis dengan --fix. Namun pastikan rule yang dipasang memang relevan. Jika terlalu banyak rule mahal atau terlalu ketat, commit akan terasa lambat.

Untuk proyek TypeScript, pastikan konfigurasi ESLint tidak memaksa analisis yang terlalu berat pada setiap commit jika memang tidak diperlukan. Beberapa setup lint berbasis type information bisa terasa lebih lambat dibanding lint biasa.

Prettier untuk konsistensi output

Prettier adalah kandidat ideal untuk pre-commit karena:

  • cepat,
  • deterministik,
  • mengurangi debat gaya penulisan di review code.

Jalankan Prettier setelah ESLint jika Anda ingin hasil akhir file tetap rapi. Dalam banyak kasus, kombinasi eslint --fix lalu prettier --write memberi hasil yang konsisten.

Type check ringan: hati-hati memilih tempatnya

Type check penuh dengan tsc --noEmit sering berguna, tetapi tidak selalu ideal di pre-commit, terutama pada proyek besar. Ada beberapa strategi yang lebih realistis:

  • Proyek kecil/menengah: jalankan tsc --noEmit di pre-commit jika waktunya masih masuk akal.
  • Proyek besar: pindahkan type check penuh ke pre-push atau CI.
  • Fallback aman: tetap jalankan lint dan format di pre-commit, lalu jalankan type check penuh di CI.

Contoh jika Anda tetap ingin menambahkan type check ke pre-commit:

#!/usr/bin/env sh
npx lint-staged
npm run typecheck

Tetapi gunakan ini hanya jika durasinya tidak mengganggu workflow. Jika developer harus menunggu lama untuk commit kecil, mereka cenderung mencari cara untuk mem-bypass hook.

Strategi Menjalankan Check Hanya pada File yang Berubah

Inilah kekuatan utama lint-staged. Daripada menjalankan command seperti eslint . untuk seluruh repository, lint-staged meneruskan daftar file staged ke command yang sesuai. Hasilnya:

  • commit lebih cepat,
  • resource lebih hemat,
  • perubahan kecil tidak memicu proses besar.

Contoh pola file yang umum:

{
  "*.{js,jsx,ts,tsx}": [
    "eslint --fix",
    "prettier --write"
  ],
  "*.{md,json,yml,yaml}": [
    "prettier --write"
  ]
}

Ada satu hal penting: tidak semua tool benar-benar cocok dijalankan per-file. Misalnya, type check TypeScript pada umumnya membutuhkan konteks proyek, bukan hanya satu file. Karena itu, strategi terbaik adalah:

  • gunakan lint-staged untuk tool yang memang aman dijalankan pada file berubah,
  • gunakan CI untuk validasi global yang bergantung pada keseluruhan proyek.

Contoh Setup yang Seimbang untuk Tim

Berikut contoh konfigurasi yang cukup aman untuk banyak tim frontend atau full-stack JavaScript/TypeScript:

{
  "scripts": {
    "prepare": "husky",
    "lint": "eslint .",
    "format": "prettier --write .",
    "typecheck": "tsc --noEmit",
    "test": "npm run test:unit"
  },
  "lint-staged": {
    "*.{js,jsx,ts,tsx}": [
      "eslint --fix",
      "prettier --write"
    ],
    "*.{json,md,css,scss,html,yml,yaml}": [
      "prettier --write"
    ]
  }
}

Isi file .husky/pre-commit:

npx lint-staged

Lalu di CI, tetap jalankan validasi penuh:

  • npm run lint
  • npm run typecheck
  • npm test

Pola ini biasanya cukup efektif: lokal untuk feedback cepat, CI untuk verifikasi final.

Praktik Tim agar Hook Tetap Cepat dan Tidak Dibenci

1. Fokus pada check yang cepat

Masukkan hanya validasi yang memberi nilai tinggi dengan waktu rendah. Prettier dan ESLint dengan scope file staged biasanya pilihan terbaik. Hindari build penuh, integrasi test, atau script berat lain di pre-commit kecuali benar-benar cepat.

2. Jadikan CI sebagai fallback wajib

Jangan bergantung penuh pada hook lokal. Semua rule penting tetap harus dijalankan di CI karena:

  • hook bisa dilewati,
  • tidak semua developer menjalankan environment identik,
  • beberapa validasi hanya benar jika dieksekusi penuh.

3. Sepakati kapan bypass boleh dilakukan

git commit --no-verify kadang dibutuhkan, misalnya saat hook rusak atau saat sedang menangani insiden. Tetapi bypass tidak boleh menjadi kebiasaan. Praktik yang umum:

  • boleh bypass hanya untuk kondisi darurat,
  • harus diperbaiki segera setelah itu,
  • CI tetap memblokir merge jika validasi penting gagal.

4. Simpan konfigurasi sesederhana mungkin

Semakin rumit hook, semakin sulit di-debug. Prioritaskan command yang jelas, sedikit, dan dapat dijalankan manual saat troubleshooting.

5. Dokumentasikan cara kerjanya

Tambahkan penjelasan singkat di README atau dokumentasi internal:

  • apa yang dijalankan saat commit,
  • cara menginstal hook,
  • cara menjalankan command yang sama secara manual,
  • cara menangani error umum.

Kesalahan Umum yang Membuat Developer Frustrasi

Hook terlalu lambat

Ini penyebab paling umum. Jika satu commit kecil butuh waktu lama, developer akan kehilangan fokus. Solusinya:

  • batasi ke file staged,
  • hapus check berat dari pre-commit,
  • pindahkan validasi global ke CI atau pre-push.

Menjalankan lint ke seluruh repo dari pre-commit

Kesalahan ini sering terjadi ketika hook berisi npm run lint yang sebenarnya memeriksa semua file. Secara teknis bisa jalan, tetapi manfaat lint-staged jadi hilang.

Urutan command tidak dipikirkan

Jika formatter dan linter sama-sama mengubah file, urutannya penting. Banyak tim memilih:

  1. eslint --fix
  2. prettier --write

Tujuannya agar perbaikan lint diterapkan dulu, lalu format akhir dirapikan secara konsisten.

Hook gagal karena command tidak tersedia

Ini biasanya terjadi jika tool diinstal global di satu mesin tetapi tidak ada di mesin lain. Gunakan dependency proyek dan panggil lewat npx atau script npm agar konsisten antar developer.

File berubah setelah commit gagal dan staging jadi membingungkan

Karena lint-staged bisa memodifikasi file, beberapa developer bingung saat commit gagal lalu melihat file ikut berubah. Ini bukan bug. Artinya formatter atau auto-fix bekerja, tetapi ada error lain yang masih harus diselesaikan. Biasanya langkah aman adalah:

  1. baca output error,
  2. cek file yang diubah otomatis,
  3. stage ulang jika perlu,
  4. commit kembali.

Type check dipaksa di semua commit meski terlalu berat

Secara teori bagus, tetapi dalam praktik sering menjadi bottleneck. Jika type check penuh memakan waktu lama, pindahkan ke CI. Jangan memaksakan validasi ideal jika efek sampingnya membuat tim sering mem-bypass hook.

Tips Debugging Saat Hook Gagal

  • Jalankan command yang sama secara manual, misalnya npx lint-staged atau npm run typecheck, untuk melihat error lebih jelas.
  • Periksa isi file hook di .husky/pre-commit agar tidak ada command yang salah atau path yang tidak valid.
  • Pastikan dependency terinstal di devDependencies, bukan mengandalkan instalasi global.
  • Cek file yang di-stage dengan git diff --cached --name-only jika terasa ada file yang tidak ikut diproses.
  • Uji pola glob pada konfigurasi lint-staged. Kadang command tidak jalan karena ekstensi file tidak cocok dengan pattern.

Kapan Perlu Menambah Hook Lain selain Pre-commit?

Jika tim Anda ingin validasi sedikit lebih kuat tanpa membebani setiap commit, pertimbangkan pemisahan tanggung jawab:

  • pre-commit: format dan lint file staged.
  • pre-push: type check penuh atau unit test yang relatif cepat.
  • CI: validasi lengkap, termasuk test suite penuh, build, dan pengecekan lint seluruh proyek.

Pemisahan ini sering lebih sehat daripada menumpuk semua proses ke satu hook.

Checklist Implementasi

  • Pasang husky dan lint-staged sebagai dev dependency.
  • Aktifkan Husky dan pastikan script prepare ada di package.json.
  • Buat hook pre-commit yang menjalankan npx lint-staged.
  • Konfigurasikan lint-staged untuk hanya memproses file staged.
  • Tambahkan ESLint dengan --fix pada file JavaScript/TypeScript.
  • Tambahkan Prettier untuk format otomatis pada file yang relevan.
  • Evaluasi apakah type check cukup cepat untuk pre-commit; jika tidak, pindahkan ke pre-push atau CI.
  • Pastikan CI tetap menjalankan lint, type check, dan test penuh.
  • Dokumentasikan cara kerja hook dan cara troubleshooting di README.
  • Sepakati kebijakan tim terkait --no-verify agar bypass tidak menjadi kebiasaan.

Jika diterapkan dengan scope yang tepat, pre-commit hook dengan Husky dan lint-staged memberi keuntungan nyata: PR lebih bersih, feedback lebih cepat, dan review bisa fokus pada logika bisnis, bukan masalah formatting atau lint yang seharusnya sudah selesai sebelum kode dikirim.