Pre-commit dengan Husky dan lint-staged membantu memindahkan pengecekan ringan ke mesin developer sebelum kode masuk ke remote dan diproses CI. Hasilnya, masalah sederhana seperti format yang tidak konsisten, error lint dasar, atau file yang seharusnya tidak lolos bisa dihentikan lebih awal tanpa menunggu pipeline penuh.

Untuk proyek JavaScript/TypeScript, pendekatan ini cocok ketika CI terasa lambat karena selalu menjalankan lint atau test secara penuh, bahkan untuk perubahan kecil. Kuncinya bukan memindahkan semua validasi ke pre-commit, melainkan memilih tugas yang cepat, deterministik, dan relevan hanya untuk file yang berubah.

Mengapa pre-commit membantu CI dan developer experience

Masalah yang umum terjadi di tim adalah:

  • CI lambat karena lint, format check, atau test dijalankan penuh di setiap push.
  • Commit kecil tertahan oleh feedback yang sebenarnya bisa diketahui dalam hitungan detik di lokal.
  • Format kode tidak konsisten karena sebagian developer lupa menjalankan formatter.

Dengan hook pre-commit, developer mendapatkan feedback sebelum commit selesai. Ini mengurangi round-trip ke server CI hanya untuk masalah dasar. Namun, ada batasnya: pre-commit bukan pengganti CI. CI tetap harus menjadi sumber kebenaran untuk validasi penuh, terutama integration test, build, dan pengecekan lint/test yang lebih mahal.

Pembagian tanggung jawab yang sehat

  • Pre-commit: tugas cepat, stabil, dan scoped ke file yang berubah.
  • CI: validasi penuh, reproducible, dan berjalan di lingkungan yang konsisten.

Pembagian ini bekerja karena waktu eksekusi dan ruang lingkup berbeda. Hook lokal menekan error sederhana sedini mungkin, sedangkan CI menangkap masalah lintas modul, dependensi antar file, dan perilaku yang tidak selalu terlihat dari perubahan kecil.

Memahami peran Husky dan lint-staged

Husky

Husky digunakan untuk mendaftarkan Git hooks seperti pre-commit, commit-msg, atau pre-push. Husky tidak melakukan linting atau formatting secara langsung; ia hanya menjadi pintu masuk agar command tertentu dieksekusi saat event Git terjadi.

lint-staged

lint-staged menjalankan command hanya pada file yang sedang di-stage. Ini penting karena menjalankan ESLint atau Prettier ke seluruh repository di setiap commit akan terasa lambat dan sering membuat developer frustrasi. Dengan membatasi scope ke file yang berubah, feedback menjadi lebih cepat dan relevan.

Kombinasi keduanya

Pola yang umum adalah:

  1. Developer melakukan git add pada file yang ingin di-commit.
  2. Hook pre-commit dari Husky berjalan.
  3. Hook memanggil lint-staged.
  4. lint-staged meneruskan file yang di-stage ke formatter dan linter yang sesuai.
  5. Jika semua lolos, commit berlanjut. Jika gagal, commit dibatalkan.

Setup step-by-step untuk proyek JavaScript/TypeScript

Contoh di bawah bersifat framework-agnostic dan cocok untuk repository Node.js biasa.

1. Pasang dependensi yang diperlukan

npm install --save-dev husky lint-staged prettier eslint

Jika proyek Anda TypeScript dan sudah memakai rule TypeScript di ESLint, tetap gunakan konfigurasi ESLint yang sudah ada. Tidak perlu mengubah arsitektur tool hanya demi hook pre-commit.

2. Tambahkan script dasar di package.json

Tambahkan script yang jelas dan bisa dijalankan manual. Ini penting untuk debugging dan konsistensi antara lokal dan CI.

{
  "scripts": {
    "prepare": "husky install",
    "lint": "eslint .",
    "lint:fix": "eslint . --fix",
    "format": "prettier . --write",
    "format:check": "prettier . --check",
    "test": "npm run test:unit",
    "test:unit": "node ./scripts/run-unit-tests.js"
  }
}

Perhatikan dua hal:

  • Script tetap bisa dijalankan tanpa Git hook. Ini memudahkan developer dan CI.
  • Hook sebaiknya memanggil command yang memang Anda pahami, bukan command kompleks yang hanya hidup di satu environment.

3. Inisialisasi Husky

npm run prepare

Setelah itu buat hook pre-commit. Pada banyak setup, file hook akan berada di direktori .husky/. Isi hook sederhananya cukup memanggil lint-staged.

npx husky add .husky/pre-commit "npx lint-staged"

Jika tim Anda memilih membuat file hook secara manual, prinsipnya sama: hindari logika shell yang terlalu rumit di dalam hook.

4. Tambahkan konfigurasi lint-staged

Konfigurasi bisa disimpan di package.json atau file terpisah. Untuk setup kecil, menyimpannya di package.json cukup praktis.

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

Konfigurasi ini berarti:

  • File JavaScript/TypeScript yang di-stage akan diformat lalu diperiksa ESLint dengan --fix.
  • File lain yang didukung formatter hanya akan dijalankan melalui Prettier.

Urutan command penting. Menjalankan Prettier sebelum ESLint sering lebih stabil untuk mengurangi noise perubahan format. Di banyak proyek, ini juga membuat hasil autofix lebih mudah dipahami.

5. Uji alur kerja

  1. Ubah satu file .ts atau .js.
  2. Stage file tersebut: git add path/to/file.ts
  3. Lakukan commit.

Jika hook aktif, lint-staged akan memproses file yang di-stage saja. Bila formatter atau linter melakukan perubahan, file biasanya akan di-update kembali dalam staged state sehingga commit tetap konsisten dengan hasil akhir tool.

Contoh package.json yang realistis

Berikut contoh ringkas yang bisa dijadikan titik awal:

{
  "name": "example-project",
  "private": true,
  "scripts": {
    "prepare": "husky install",
    "lint": "eslint .",
    "lint:fix": "eslint . --fix",
    "format": "prettier . --write",
    "format:check": "prettier . --check",
    "test": "npm run test:unit",
    "test:unit": "node ./scripts/run-unit-tests.js"
  },
  "lint-staged": {
    "*.{js,jsx,ts,tsx}": [
      "prettier --write",
      "eslint --fix"
    ],
    "*.{json,md,css,scss,yml,yaml}": [
      "prettier --write"
    ]
  },
  "devDependencies": {
    "eslint": "^0.0.0",
    "husky": "^0.0.0",
    "lint-staged": "^0.0.0",
    "prettier": "^0.0.0"
  }
}

Nomor versi di atas sengaja tidak dijadikan acuan. Ikuti versi yang kompatibel dengan proyek Anda, lalu baca dokumentasi resmi bila ada perubahan perilaku antar versi.

Menjalankan formatter dan linter hanya pada file yang berubah

Nilai utama pre-commit dengan Husky dan lint-staged ada pada pembatasan scope. Menjalankan eslint . atau prettier . --write untuk seluruh repository saat pre-commit adalah anti-pattern untuk banyak tim karena:

  • Waktu commit membengkak.
  • Developer bisa tanpa sengaja memformat file yang tidak sedang dikerjakan.
  • Commit menjadi penuh noise dan sulit direview.

lint-staged menghindari masalah ini dengan mengambil daftar file yang sudah di-stage dari Git, lalu meneruskannya ke command yang relevan. Ini membuat hook lebih cepat dan hasil commit lebih fokus.

Kapan cukup memakai formatter + linter saja

Untuk mayoritas proyek, pre-commit sebaiknya minimal berisi:

  • Formatter untuk konsistensi gaya kode.
  • Linter dengan autofix untuk error sintaks sederhana, import tidak terpakai, atau rule kualitas dasar.

Ini memberi rasio manfaat terhadap waktu terbaik. Banyak masalah tertangkap cepat tanpa membuat commit terasa berat.

Test parsial vs test penuh di CI

Pertanyaan yang sering muncul: apakah test juga perlu dijalankan saat pre-commit? Jawabannya tergantung biaya dan keandalan test Anda.

Test parsial di lokal

Test parsial masuk akal jika:

  • Runner test Anda bisa menarget file terkait perubahan dengan andal.
  • Durasi test cukup singkat.
  • Hubungan antara file berubah dan test terkait relatif jelas.

Contohnya, sebagian tim memilih menjalankan unit test yang berhubungan dengan file yang diubah pada pre-push, bukan pre-commit. Alasannya sederhana: commit terjadi lebih sering daripada push, jadi hook commit harus dijaga tetap sangat cepat.

Test penuh di CI

CI tetap perlu menjalankan test penuh karena:

  • Perubahan kecil bisa memengaruhi area lain yang tidak terlihat dari file yang di-stage.
  • Lingkungan lokal developer bisa berbeda.
  • Sebagian test integrasi atau build check terlalu mahal untuk dijalankan di setiap commit.

Rekomendasi praktis

  • Pre-commit: formatter + lint pada staged files.
  • Opsional pre-push: subset test yang cepat dan cukup stabil.
  • CI: lint penuh bila diperlukan, test penuh, build, dan validasi release lainnya.

Pola ini menjaga feedback lokal cepat tanpa mengorbankan jaring pengaman di server.

Trade-off dan keterbatasan

Keuntungan utama

  • Feedback lebih cepat sebelum menunggu CI.
  • Commit lebih bersih dan konsisten.
  • Noise review berkurang karena formatter berjalan otomatis.
  • CI bisa lebih fokus pada validasi yang memang mahal.

Keterbatasan yang perlu dipahami

  • Hook lokal tidak identik dengan CI. Developer bisa menonaktifkan hook atau memiliki environment berbeda.
  • Rule lint tertentu tetap mahal, terutama yang melibatkan analisis tipe atau cakupan proyek luas.
  • Autofix tidak selalu aman. Sebagian perubahan otomatis perlu tetap ditinjau.
  • Tidak semua test cocok dijalankan parsial. Dependency graph antar modul bisa membuat hasil parsial menyesatkan.

Karena itu, target utamanya bukan memindahkan semua validasi ke lokal, melainkan memindahkan validasi yang murah dan bernilai tinggi.

Jebakan umum saat mengadopsi Husky dan lint-staged

1. Hook terlalu berat

Kesalahan paling umum adalah memasukkan terlalu banyak pekerjaan ke pre-commit: lint seluruh repo, type-check penuh, build, dan test lengkap sekaligus. Akibatnya commit menjadi lambat dan developer mencari cara untuk melewati hook.

Jika hook terasa menghambat alur kerja sehari-hari, kemungkinan isinya terlalu berat atau tidak stabil.

2. Bergantung pada command yang tidak tersedia di semua mesin

Hindari command shell yang spesifik OS bila tim Anda memakai campuran macOS, Linux, dan Windows. Sebisa mungkin panggil executable dari tool Node yang ada di node_modules melalui script npm atau langsung lewat lint-staged.

3. Mengubah file di luar staged scope

Formatter atau script kustom yang memproses seluruh direktori bisa memodifikasi file yang tidak sedang di-commit. Ini membingungkan dan berisiko memasukkan perubahan tak sengaja. Pastikan command memang menerima daftar file dari lint-staged.

4. Hook tidak sinkron dengan aturan CI

Jika rule ESLint di lokal berbeda dari CI, developer akan menerima feedback yang kontradiktif. Simpan konfigurasi lint/format dalam repository dan gunakan command yang seragam.

5. Mengandalkan autofix untuk semua masalah

Tidak semua pelanggaran lint bisa atau seharusnya diperbaiki otomatis. Jika ada rule yang sering menimbulkan false positive atau mengubah kode secara agresif, evaluasi ulang rule tersebut sebelum menaruhnya di hook.

Strategi adopsi bertahap di tim

Adopsi yang terlalu agresif sering menimbulkan resistensi, terutama pada repository lama yang belum konsisten formatting-nya.

Mulai dari dua hal paling aman

  1. Tambahkan Prettier untuk file staged.
  2. Tambahkan ESLint --fix hanya untuk ekstensi yang memang sudah stabil.

Ini biasanya cukup untuk memberi manfaat cepat tanpa banyak konflik.

Hindari big bang reformat saat tidak perlu

Memformat seluruh repository sekaligus bisa mempersulit blame Git dan membuat pull request lain konflik. Bila codebase besar, pertimbangkan strategi incremental: file diformat ketika disentuh oleh perubahan baru.

Dokumentasikan escape hatch, tetapi jangan dijadikan kebiasaan

Ada situasi darurat ketika developer perlu melewati hook, misalnya untuk investigasi atau hotfix tertentu. Praktik aman adalah mendokumentasikan kapan bypass boleh dilakukan dan memastikan CI tetap memverifikasi semuanya. Jangan mendesain proses yang memaksa orang sering memakai bypass; itu tanda workflow perlu diperbaiki.

Praktik aman agar hook tidak rapuh

1. Jaga hook tetap kecil

Gunakan hook sebagai orkestrator, bukan tempat logika bisnis. Idealnya hook hanya memanggil command yang sudah ada, misalnya lint-staged.

2. Pastikan semua command bisa dijalankan manual

Jika developer tidak bisa menjalankan command yang sama dari terminal, debugging akan sulit. Script npm yang eksplisit membantu mengurangi kebingungan.

3. Hindari ketergantungan pada state lokal yang tidak pasti

Jangan membuat hook bergantung pada service yang belum tentu hidup, credential tertentu, atau network call yang rentan gagal. Hook pre-commit sebaiknya lokal, cepat, dan deterministik.

4. Bedakan validasi cepat dan validasi lengkap

Simpan pengecekan mahal seperti full type-check, build produksi, atau integration test untuk CI atau paling jauh pre-push jika memang masuk akal.

5. Ukur pengalaman developer, bukan hanya kepatuhan tool

Setelah diadopsi, perhatikan berapa lama commit rata-rata terasa, jenis error apa yang paling sering tertangkap, dan apakah CI benar-benar lebih bersih. Hook yang bagus bukan yang paling banyak menjalankan tugas, tetapi yang paling efektif memberi feedback cepat tanpa mengganggu kerja.

Contoh pembagian workflow yang seimbang

  • Saat pre-commit: prettier --write dan eslint --fix untuk file staged.
  • Saat pre-push (opsional): unit test cepat atau subset test yang relevan.
  • Saat CI: install bersih, lint penuh bila perlu, type-check penuh, seluruh unit/integration test, dan build.

Dengan pola ini, commit kecil tidak tertahan oleh proses berat, sementara pipeline CI tetap menjaga kualitas akhir repository.

Penutup

Pre-commit dengan Husky dan lint-staged efektif ketika digunakan untuk hal yang tepat: validasi ringan, cepat, dan terbatas pada file yang berubah. Untuk proyek JavaScript/TypeScript, kombinasi formatter dan linter pada staged files biasanya sudah memberi peningkatan besar pada developer experience sekaligus mengurangi kegagalan CI yang sebenarnya bisa dicegah lebih awal.

Mulailah dari setup sederhana, pastikan hook stabil di semua mesin tim, lalu evaluasi apakah perlu menambah validasi lain. Jika commit tetap cepat dan CI tetap menjadi sumber verifikasi penuh, Anda mendapatkan keseimbangan yang sehat antara feedback lokal dan keandalan pipeline.