chi — router yang tetap net/http
chi menambahkan satu hal yang benar-benar kurang di ServeMux: grup rute dengan middleware sendiri. Selain itu ia sengaja tidak menambahkan apa-apa — handler-mu tetap fungsi net/http biasa.
Intisari
- Handler chi adalah
http.HandlerFunc. Tidak ada tipe konteks baru yang harus dipelajari. r.Groupdanr.Routememberi middleware per kelompok rute — kegunaan utamanya.- Paket
chi/middlewaremenyediakan RequestID, RealIP, Recoverer, Timeout, dan pembungkusResponseWriteryang benar. chi.Walkmencetak seluruh tabel rute — diagnostik yang tidak ada di ServeMux.- Nol dependensi di luar pustaka standar.
Bentuk rutenya
import (
"github.com/go-chi/chi/v5"
"github.com/go-chi/chi/v5/middleware"
)
func (h *Handler) Rute() http.Handler {
r := chi.NewRouter()
// Middleware global — urutan dari luar ke dalam (Fase 3)
r.Use(middleware.RequestID)
r.Use(middleware.RealIP)
r.Use(h.Log)
r.Use(middleware.Recoverer)
r.Use(middleware.Timeout(10 * time.Second))
r.Use(h.HeaderKeamanan)
r.Get("/sehat", h.Sehat)
r.Route("/api/v1", func(r chi.Router) {
// Publik
r.Group(func(r chi.Router) {
r.Use(httprate.LimitByIP(60, time.Minute))
r.Get("/produk", h.DaftarProduk)
r.Get("/produk/{id}", h.AmbilProduk)
r.Post("/masuk", h.Masuk)
})
// Butuh sesi
r.Group(func(r chi.Router) {
r.Use(h.WajibLogin)
r.Post("/produk", h.BuatProduk)
r.Put("/produk/{id}", h.UbahProduk)
r.Delete("/produk/{id}", h.HapusProduk)
r.Route("/pesanan", func(r chi.Router) {
r.Get("/", h.DaftarPesanan)
r.Post("/", h.BuatPesanan)
r.Route("/{pesananID}", func(r chi.Router) {
r.Use(h.MuatPesanan) // memuat + memeriksa kepemilikan
r.Get("/", h.AmbilPesanan)
r.Post("/batal", h.BatalPesanan)
})
})
})
// Hanya admin
r.Group(func(r chi.Router) {
r.Use(h.WajibLogin, h.WajibPeran("admin"))
r.Get("/admin/laporan", h.Laporan)
})
})
return r
}
Inilah nilai chi dalam satu tangkapan layar. Dengan ServeMux, tiga kelompok di atas berarti
tiga http.NewServeMux() terpisah plus StripPrefix di setiap pemasangan.
Dengan chi, batas otorisasinya terbaca langsung dari indentasi — dan itu berarti reviewer bisa
melihat rute mana yang tidak terlindungi.
Handler tetap seperti biasa
// Tidak ada tipe chi di sini. Fungsi ini bisa dipindah ke server
// ServeMux polos tanpa satu pun perubahan.
func (h *Handler) AmbilProduk(w http.ResponseWriter, r *http.Request) {
id, err := strconv.ParseInt(chi.URLParam(r, "id"), 10, 64)
if err != nil {
h.tulisError(w, r, ErrIDTidakValid)
return
}
...
}
Middleware yang memuat sumber daya
// Dipasang di r.Route("/{pesananID}"): berlaku untuk SEMUA rute anaknya,
// jadi tidak ada endpoint yang bisa lupa memeriksa kepemilikan.
func (h *Handler) MuatPesanan(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
id, err := strconv.ParseInt(chi.URLParam(r, "pesananID"), 10, 64)
if err != nil {
h.tulisError(w, r, ErrIDTidakValid)
return
}
u := auth.DariContext(r.Context())
p, err := h.pesanan.AmbilMilik(r.Context(), id, u.ID) // Fase 6
if err != nil {
h.tulisError(w, r, err)
return
}
ctx := context.WithValue(r.Context(), kunciPesanan, p)
next.ServeHTTP(w, r.WithContext(ctx))
})
}
Pola ini menutup satu kelas kerentanan secara struktural. Karena pemeriksaan kepemilikan dipasang di titik cabang rute, endpoint baru yang ditambahkan di bawahnya otomatis terlindungi. Endpoint yang lupa diperiksa hanya mungkin kalau seseorang sengaja mendaftarkannya di luar grup — dan itu terlihat jelas di diff.
Middleware bawaan yang layak dipakai
| Middleware | Fungsinya |
|---|---|
middleware.RequestID | ID unik per permintaan, masuk ke context |
middleware.RealIP | Menyetel RemoteAddr dari X-Forwarded-For — hanya di belakang proxy tepercaya |
middleware.Recoverer | Pemulih panic |
middleware.Timeout | Tenggat pada context permintaan |
middleware.Compress | gzip untuk respons |
middleware.NewWrapResponseWriter | Pembungkus yang benar — meneruskan Flush dan Hijack (Fase 3) |
RealIP berbahaya kalau dipasang tanpa proxy di depan. Ia mempercayai
X-Forwarded-For apa adanya, sehingga siapa pun bisa mengarang alamat IP dan melewati rate
limit maupun daftar blokir berbasis IP. Pasang hanya kalau aplikasimu memang selalu di belakang ALB atau
CloudFront yang menimpa header itu.
Melihat tabel rute
chi.Walk(r, func(metode, rute string, h http.Handler,
mw ...func(http.Handler) http.Handler) error {
log.Info("rute", "metode", metode, "path", rute, "middleware", len(mw))
return nil
})
GET /sehat middleware=6
GET /api/v1/produk middleware=7
POST /api/v1/produk middleware=7
GET /api/v1/pesanan/{pesananID}/ middleware=8
Cetak ini saat startup di staging. Ia menjawab dua pertanyaan sekaligus: apakah rute yang baru kamu tambahkan benar-benar terdaftar, dan apakah rute sensitif punya jumlah middleware yang kamu harapkan. Rute yang jumlah middleware-nya lebih kecil dari tetangganya hampir selalu rute yang lupa dilindungi.
Berpindah dari ServeMux
| ServeMux | chi |
|---|---|
mux.HandleFunc("GET /produk/{id}", h.Ambil) | r.Get("/produk/{id}", h.Ambil) |
r.PathValue("id") | chi.URLParam(r, "id") |
Subrouter + StripPrefix | r.Route("/api", func(r chi.Router){...}) |
| Bungkus manual | r.Use(...) |
| Handler | Tidak berubah sama sekali |
Latihan: ubah router ServeMux-mu ke chi tanpa menyentuh satu pun handler. Tambahkan
chi.Walk yang mencetak tabel rute saat startup, lalu sengaja pindahkan satu endpoint tulis
keluar dari grup WajibLogin dan temukan perbedaannya di keluaran itu.
Rangkuman ini sengaja dipangkas ke bagian yang dipakai di roadmap. Buka sumber aslinya saat kamu butuh detail lengkap atau referensi parameter.