← Semua pembelajaran / Go Nol → Enterprise
Fase 7 · Framework Web & Frontend

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.

Sumber asli pkg.go.dev Resmi Rangkuman ~6 menit baca

Intisari

  • Handler chi adalah http.HandlerFunc. Tidak ada tipe konteks baru yang harus dipelajari.
  • r.Group dan r.Route memberi middleware per kelompok rute — kegunaan utamanya.
  • Paket chi/middleware menyediakan RequestID, RealIP, Recoverer, Timeout, dan pembungkus ResponseWriter yang benar.
  • chi.Walk mencetak 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

MiddlewareFungsinya
middleware.RequestIDID unik per permintaan, masuk ke context
middleware.RealIPMenyetel RemoteAddr dari X-Forwarded-For — hanya di belakang proxy tepercaya
middleware.RecovererPemulih panic
middleware.TimeoutTenggat pada context permintaan
middleware.Compressgzip untuk respons
middleware.NewWrapResponseWriterPembungkus 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

ServeMuxchi
mux.HandleFunc("GET /produk/{id}", h.Ambil)r.Get("/produk/{id}", h.Ambil)
r.PathValue("id")chi.URLParam(r, "id")
Subrouter + StripPrefixr.Route("/api", func(r chi.Router){...})
Bungkus manualr.Use(...)
HandlerTidak 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.