Progress belajar
Modul 15 dari 73
0% 0/73 modul selesai
Setelah selesai, tandai modul ini agar progres kursus tetap rapi.
Progress disimpan lokal di browser ini.
Routing dengan chi
REST yang Rapi dan Tumbuh
Di modul ini kita naik dari handler net/http murni ke routing REST yang enak dirawat: grouping /v1, subrouter per domain, dan middleware chain untuk backend online shop skincare.
Kenapa chi?
Dari handler tunggal ke peta API yang jelas
Di modul sebelumnya kamu sudah menulis handler dengan standard library net/http. Sekarang kita butuh cara menyusun puluhan endpoint tanpa membuat main.go jadi papan tempel route yang panjang.
Di Express.js kamu terbiasa memakai express.Router() untuk memecah route produk, cart, dan order ke file terpisah. Di Laravel kamu menulis grup route di routes/api.php dengan Route::prefix('v1')->group(...). chi memberi pengalaman serupa di Go, tetapi tetap berdiri di atas net/http. Router chi pada akhirnya hanyalah sebuah http.Handler, jadi bisa langsung diberikan ke http.ListenAndServe tanpa lapisan ajaib di tengah.
Dokumentasi resmi chi menyebut dirinya router yang ringan, idiomatik, dan composable untuk HTTP service Go. Artinya chi bukan framework besar seperti Laravel atau NestJS. Ia fokus pada tiga hal: mencocokkan route, menyusun middleware, dan mengkomposisi handler. Validasi, business logic, dan akses database tetap urusan kamu, persis seperti di modul net/http.
Anggap chi seperti express.Router(), tetapi state request tidak pernah disimpan di object router. Data request tetap hidup di *http.Request, path parameter dibaca dari context route, dan handler tetap berbentuk func(w http.ResponseWriter, r *http.Request) yang sama persis seperti modul lalu.
Tetap http.Handler
Router, subrouter, dan middleware chi semuanya http.Handler, jadi httptest dan tooling standard library tetap jalan.
Grouping dan versioning
Prefix /v1, group customer, dan group admin tersusun rapi tanpa mengulang string path di tiap endpoint.
Middleware ergonomis
RequestID, Logger, Recoverer, Timeout, dan auth dirangkai per group dengan satu r.Use.
Mount per domain
Tiap domain (produk, cart, order) mengekspos router sendiri lalu dipasang ke root router lewat Mount.
Modul ini memakai import github.com/go-chi/chi/v5 (versi v5.3.0), bukan github.com/go-chi/chi lama tanpa suffix /v5. Module proyek kita adalah github.com/kamu/skincare-backend dengan go 1.26 di go.mod.
Mental Model Router
Router adalah dispatcher, bukan tempat business logic
Tugas router hanya satu: memilih handler berdasarkan method dan path. Ia tidak menghitung diskon, tidak mengecek stok, dan tidak menyimpan order.
Komponen yang menerima HTTP request, mencocokkan kombinasi method dan path, lalu memanggil handler yang sesuai. Tidak lebih dari itu.
Dalam backend skincare, GET /v1/products harus jatuh ke handler katalog, dan POST /v1/checkout harus jatuh ke handler checkout. Router menjaga peta ini tetap eksplisit dan mudah dibaca, sehingga siapa pun yang membuka kode tahu endpoint apa saja yang ada hanya dengan melihat definisi route.
router.get('/products/:id', handler)memakai callback(req, res)atau closure controller.- Middleware bisa menempel data langsung ke
reqdengan pola bebas. - Banyak aplikasi mencampur routing, validasi, dan business logic di file route yang sama.
r.Get("/products/{id}", handler)menerima pattern danhttp.HandlerFuncstandar.- Data request lewat
*http.Requestdan context, bukan object global yang bisa dimutasi sembarangan. - Router idealnya hanya wiring; validasi dan business logic pindah ke handler atau service.
chi tidak mengubah cara kerja HTTP di Go. Ia hanya membuat pencocokan route, pembacaan path parameter, grouping, mounting, dan middleware terasa jauh lebih ergonomis dibanding menyusunnya manual di atas http.ServeMux.
flowchart LR Client["React Frontend"] -->|HTTP JSON| Router["chi Router"] Router -->|GET /v1/products| ProductHandler["Product Handler"] Router -->|POST /v1/cart/items| CartHandler["Cart Handler"] Router -->|POST /v1/checkout| OrderHandler["Order Handler"] ProductHandler --> ProductService["Product Service"] CartHandler --> CartService["Cart Service"] OrderHandler --> OrderService["Order Service"]
Gambar 1. Router hanya memilih handler. Aturan bisnis domain hidup di service, yang baru kita rapikan di Roadmap 4.
Di sini kita pakai helper sederhana writeJSON (menulis payload apa adanya) dan writeError(w, status, message) yang menghasilkan {"error":"pesan"}. Envelope response yang konsisten dan terstruktur (data, meta, error dengan kode) baru dirancang di modul berikutnya, Desain Request & Response. Fokus modul ini adalah routing, bukan format JSON-nya.
Method Route REST
GET, POST, PUT, PATCH, DELETE sebagai method helper
chi menyediakan helper r.Get, r.Post, r.Put, r.Patch, dan r.Delete agar mapping REST terbaca langsung dari kode.
Setiap helper menerima pattern path dan handler function. Bentuk handler tetap sama persis seperti net/http, jadi seluruh pengetahuan dari modul sebelumnya tetap dipakai tanpa perubahan.
internal/product/routes.gor.Get("/products", h.List) r.Post("/products", h.Create) r.Get("/products/{id}", h.GetByID) r.Put("/products/{id}", h.Replace) r.Patch("/products/{id}", h.UpdatePartial) r.Delete("/products/{id}", h.Delete)
Untuk API skincare, pemilihan method menyatakan niat endpoint dengan jelas. Frontend dan mobile tahu apa yang akan terjadi hanya dari kombinasi method dan path.
/v1/products Daftar produk skincare, dengan filter kategori, q (pencarian), dan pagination /v1/admin/products Buat produk baru dari admin dashboard /v1/admin/products/{id} Ganti representasi produk secara penuh /v1/admin/products/{id} Ubah sebagian data produk, misalnya stok atau status /v1/admin/products/{id} Hapus atau nonaktifkan produk dari katalog Di Laravel, Route::apiResource('products', ...) membuat lima route REST sekaligus secara otomatis. chi sengaja tidak melakukan itu; kamu menulis tiap method helper satu per satu. Lebih verbose, tetapi setiap route eksplisit dan tidak ada method tersembunyi yang muncul tanpa kamu sadari.
Pakai method helper untuk route REST umum. Gunakan r.Method(verb, pattern, handler) atau r.MethodFunc hanya saat kamu benar-benar perlu verb non-standar. Untuk satu handler yang melayani semua method pada satu path, ada r.HandleFunc, tetapi jarang dibutuhkan di REST.
Path Parameter dengan URLParam
Membaca id dari URL secara eksplisit lalu memvalidasinya
Path parameter dipakai ketika identitas resource menjadi bagian dari URL: produk tertentu, item cart tertentu, atau order tertentu.
Di Express kamu membaca req.params.id, di Laravel argumen method controller. Di chi, parameter URL dibaca dengan chi.URLParam(r, "id"). Nilainya selalu string, jadi untuk ID numerik kita parse dan validasi secara eksplisit sebelum dipakai.
internal/product/handler.gofunc productIDFromRequest(r *http.Request) (int64, error) { rawID := chi.URLParam(r, "id") id, err := strconv.ParseInt(rawID, 10, 64) if err != nil || id <= 0 { return 0, fmt.Errorf("product id tidak valid: %q", rawID) } return id, nil }
req.params.id di Express terasa seperti field siap pakai yang sudah tersedia. Di chi, parameter diambil dari route context yang ditanam chi ke dalam *http.Request. Hasilnya tetap eksplisit dan dekat dengan model net/http, bukan sihir framework.
Jangan langsung memakai string dari URL untuk query database. Parse ke tipe yang tepat, validasi (di sini menolak ID <= 0), lalu kirim int64 ke service atau repository. Tanpa ini, input /v1/products/abc atau /v1/products/-1 bisa lolos ke layer bawah.
Selain {id}, chi mendukung wildcard * untuk menangkap sisa path (mis. r.Get("/files/*", ...) lalu chi.URLParam(r, "*")), dan pattern bertipe regex seperti {id:[0-9]+}. Untuk REST biasa, parameter bernama plus validasi manual sudah cukup dan lebih mudah dibaca.
Grouping, Mount, dan Versioning
Menyusun route berdasarkan prefix dan domain
Route grouping membuat prefix seperti /v1 tidak diulang di setiap endpoint, dan mounting membuat tiap domain bisa hidup di package sendiri.
Versioning penting karena client mobile dan frontend bisa hidup lebih lama daripada satu siklus deploy backend. Ketika nanti ada perubahan kontrak besar, kita bisa menambah /v2 tanpa mematahkan client lama yang masih memanggil /v1.
Ada dua alat yang sering tertukar: r.Route dan r.Mount. Memahami bedanya adalah kunci struktur router yang bersih.
- Membuat subrouter inline lewat closure
func(r chi.Router). - Cocok untuk grouping lokal di file yang sama, mis. prefix
/v1di root router. - Kamu tetap menulis tiap route di dalam closure tersebut.
- Memasang sebuah
http.Handler(biasanya router dari package lain) di bawah prefix. - Cocok saat subrouter datang dari domain lain, mis.
internal/product. - Domain itu tidak tahu prefix
/v1; ia hanya tahu route relatifnya sendiri (/,/{id}).
Berikut root router yang memakai keduanya. Route("/v1", ...) membuat grup versi, lalu di dalamnya Mount memasang router tiap domain.
internal/router/router.gofunc New(products, cart, orders, payments http.Handler) http.Handler { r := chi.NewRouter() r.Route("/v1", func(r chi.Router) { r.Mount("/products", products) r.Mount("/cart", cart) r.Mount("/orders", orders) r.Mount("/payments", payments) }) return r }
Karena Mount menempel router produk di /products, di dalam internal/product kamu cukup menulis route relatif / untuk koleksi dan /{id} untuk detail. Path penuh terbentuk dari gabungan prefix mount dan route relatif.
internal/product/handler.gofunc (h *Handler) Routes() chi.Router { r := chi.NewRouter() r.Get("/", h.List) // -> GET /v1/products r.Get("/{id}", h.GetByID) // -> GET /v1/products/{id} return r }
flowchart TD
Root["chi Root Router"] --> V1["Route /v1"]
V1 --> P["Mount /products<br/>(internal/product)"]
V1 --> C["Mount /cart<br/>(internal/cart)"]
V1 --> O["Mount /orders<br/>(internal/order)"]
V1 --> Pay["Mount /payments<br/>(internal/payment)"]
P --> P1["GET / -> /v1/products"]
P --> P2["GET /{id} -> /v1/products/{id}"]Gambar 2. Prefix /v1 ditulis sekali di root. Tiap domain hanya tahu route relatifnya sendiri, sehingga pindah versi cukup menyentuh satu tempat.
Di Laravel, Route::prefix('v1')->group(fn () => ...) adalah padanan langsung r.Route("/v1", func(r chi.Router) { ... }). Memasang router dari package lain dengan r.Mount("/products", productRouter) mirip memuat file route terpisah; bedanya di Go yang dipasang adalah sebuah http.Handler konkret, bukan referensi file.
Jangan membuat versi baru untuk perubahan kecil yang backward compatible (menambah field response, menambah endpoint baru). Buat /v2 hanya saat kontrak response berubah cara baca-nya, behavior utama bergeser, atau lifecycle resource berubah besar sehingga client lama akan rusak.
Middleware Chain dan Urutannya
Layer lintas request sebelum sampai ke handler
Middleware adalah fungsi yang membungkus handler untuk menjalankan logic lintas route: request id, logging, panic recovery, timeout, auth, dan CORS.
Signature middleware Go selalu sama: menerima http.Handler, mengembalikan http.Handler. Inilah alasan middleware chi tetap kompatibel dengan standard library, dan kenapa middleware yang kamu tulis sendiri di modul net/http tetap bisa dipakai di chi tanpa diubah.
middleware-signature.gofunc Middleware(next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { // logic SEBELUM handler berjalan next.ServeHTTP(w, r) // logic SETELAH handler selesai }) }
chi membawa kumpulan middleware bawaan di subpackage github.com/go-chi/chi/v5/middleware. Untuk root router proyek, set inti yang aman dan idiomatik adalah RequestID, Logger, Recoverer, dan Timeout.
internal/router/router.gor := chi.NewRouter() r.Use(middleware.RequestID) // beri setiap request id unik r.Use(middleware.Logger) // log method, path, status, durasi r.Use(middleware.Recoverer) // ubah panic jadi 500, bukan crash server r.Use(middleware.Timeout(15 * time.Second)) // batasi durasi request
Banyak tutorial lama menaruh middleware.RealIP di sini. Hindari. RealIP kini ditandai deprecated karena rawan IP spoofing (advisory GHSA-3fxj-6jh8-hvhx, severity Critical): ia memutasi r.RemoteAddr dan memercayai header X-Forwarded-For apa adanya, padahal header itu bisa dipalsukan client. Pakai middleware client-IP yang eksplisit sesuai infrastruktur (mis. ClientIPFromXFFTrustedProxies(n) bila di belakang ALB atau proxy tepercaya), lalu baca hasilnya lewat GetClientIP(ctx). Kita bahas tuntas di modul Middleware.
Urutan r.Use menentukan urutan eksekusi: middleware yang didaftarkan lebih dulu membungkus yang setelahnya, seperti lapisan bawang. Recoverer harus berada cukup luar agar bisa menangkap panic dari middleware dan handler di dalamnya, dan RequestID di paling luar agar setiap log sudah membawa id.
sequenceDiagram participant Client as React Client participant ReqID as RequestID participant Log as Logger participant Recover as Recoverer participant Handler as Product Handler Client->>ReqID: GET /v1/products ReqID->>Log: tanam request id ke context Log->>Recover: catat awal request Recover->>Handler: lindungi dari panic, lalu teruskan Handler-->>Recover: tulis JSON response Recover-->>Log: response lewat tanpa panic Log-->>Client: 200 OK + durasi tercatat
Gambar 3. Middleware membungkus dari luar ke dalam. Request menembus tiap lapisan menuju handler, lalu response kembali menembus lapisan yang sama dalam urutan terbalik.
Express middleware memanggil next() untuk meneruskan. Di Go, middleware memanggil next.ServeHTTP(w, r). Konsepnya identik (kontrol diteruskan ke lapisan berikutnya), tetapi bentuknya interface http.Handler, bukan function dengan callback next.
Tidak semua middleware harus global. chi punya dua cara memasang middleware pada sebagian route saja: r.Group (untuk beberapa route tanpa menambah prefix) dan r.With (untuk satu route).
internal/router/router.gor.Route("/v1", func(r chi.Router) { // Route publik: tanpa auth r.Get("/products", productHandler.List) r.Get("/products/{id}", productHandler.GetByID) r.Post("/payments/webhook", paymentHandler.Webhook) // publik, verifikasi signature provider // Group route yang butuh login customer (tanpa prefix tambahan) r.Group(func(r chi.Router) { r.Use(authMiddleware) // hanya berlaku di group ini r.Get("/cart", cartHandler.Get) r.Post("/cart/items", cartHandler.AddItem) r.Post("/checkout", orderHandler.Checkout) }) // Satu route dengan middleware tambahan via With r.With(rateLimitWebhook).Post("/payments/refund", paymentHandler.Refund) })
r.Route(prefix, fn) membuat subrouter berprefix. r.Group(fn) membuat subrouter tanpa prefix, ideal untuk menerapkan middleware (mis. auth) ke sekumpulan route yang path-nya berbeda-beda. r.With(mw...) menempel middleware ke satu route tunggal. Webhook pembayaran sengaja diletakkan publik di luar group auth karena dipanggil server provider, bukan user login. Detail webhook lengkap kita rakit di modul peta API final.
Pada chi, r.Use(...) wajib dipanggil sebelum route apa pun didefinisikan di router atau group yang sama. Mendaftarkan middleware setelah route akan memicu panic saat startup. Letakkan semua r.Use di paling atas blok router.
NotFound dan MethodNotAllowed
Balas 404 dan 405 dengan JSON, bukan teks polos
Secara default, chi membalas route tak dikenal dengan teks 404 page not found dan method salah dengan 405 method not allowed polos. Untuk API JSON, dua respons ini sebaiknya konsisten dengan format error endpoint lain.
Frontend React kamu hampir pasti menjalankan await response.json() untuk setiap respons. Bila 404 mengembalikan teks polos, parsing JSON di sisi client gagal dan pesan error jadi membingungkan. Maka kita pasang handler kustom di root router.
internal/router/router.gor.NotFound(func(w http.ResponseWriter, r *http.Request) { writeError(w, http.StatusNotFound, "rute tidak ditemukan") }) r.MethodNotAllowed(func(w http.ResponseWriter, r *http.Request) { writeError(w, http.StatusMethodNotAllowed, "method tidak diizinkan untuk rute ini") })
Di Express kamu menulis app.use((req, res) => res.status(404).json(...)) di paling akhir. Di Laravel ada Route::fallback(...) dan exception handler untuk MethodNotAllowedHttpException. r.NotFound dan r.MethodNotAllowed chi adalah padanan langsungnya, hanya saja terdaftar di router, bukan sebagai route terakhir.
404 Not Found berarti tidak ada route yang cocok dengan path sama sekali. 405 Method Not Allowed berarti path-nya ada tetapi method-nya salah, misalnya DELETE /v1/products padahal hanya GET dan POST yang terdaftar di path itu. chi membedakan keduanya otomatis selama kamu mendaftarkan kedua handler ini.
Struktur Router Proyek Skincare
Root router di satu tempat, domain router di package masing-masing
Struktur yang rapi membuat route mudah ditemukan tanpa membuat setiap package saling bergantung.
- cmd/
- api/
- main.go entry point server HTTP
- internal/
- router/
- router.go root router: middleware global, versioning, mount
- product/
- handler.go route katalog produk
- cart/
- handler.go route keranjang (butuh login)
- order/
- handler.go route checkout dan order
- payment/
- handler.go webhook pembayaran (publik)
- go.mod
Root router mengurus hal lintas domain: middleware global, health check, versioning, mount subrouter, plus NotFound dan MethodNotAllowed. Tiap domain package mengurus route miliknya sendiri dan tidak tahu prefix /v1.
go.modmodule github.com/kamu/skincare-backend go 1.26 require github.com/go-chi/chi/v5 v5.3.0
internal/router/router.gopackage router import ( "encoding/json" "net/http" "time" "github.com/go-chi/chi/v5" "github.com/go-chi/chi/v5/middleware" ) func New(products, cart, orders, payments http.Handler) http.Handler { r := chi.NewRouter() // Middleware global: daftarkan SEBELUM route apa pun. r.Use(middleware.RequestID) r.Use(middleware.Logger) r.Use(middleware.Recoverer) r.Use(middleware.Timeout(15 * time.Second)) // Respons 404 dan 405 yang konsisten dalam bentuk JSON. r.NotFound(func(w http.ResponseWriter, r *http.Request) { writeError(w, http.StatusNotFound, "rute tidak ditemukan") }) r.MethodNotAllowed(func(w http.ResponseWriter, r *http.Request) { writeError(w, http.StatusMethodNotAllowed, "method tidak diizinkan untuk rute ini") }) r.Get("/healthz", healthz) r.Route("/v1", func(r chi.Router) { r.Mount("/products", products) r.Mount("/cart", cart) r.Mount("/orders", orders) r.Mount("/payments", payments) }) return r } func healthz(w http.ResponseWriter, r *http.Request) { writeJSON(w, http.StatusOK, map[string]string{"status": "ok"}) } func writeJSON(w http.ResponseWriter, status int, payload any) { w.Header().Set("Content-Type", "application/json") w.WriteHeader(status) _ = json.NewEncoder(w).Encode(payload) } func writeError(w http.ResponseWriter, status int, message string) { writeJSON(w, status, map[string]string{"error": message}) }
Dengan menerima http.Handler, root router tidak peduli apakah router produk berasal dari chi, http.ServeMux, atau handler buatan sendiri. Ini menjaga dependency antar package tetap rendah dan membuat tiap domain bisa dites terisolasi.
Modul ini menyusun kerangkanya: /v1/products dan /v1/products/{id} publik, group /v1/cart, /v1/checkout, dan /v1/orders butuh login customer, group /v1/admin/products butuh role admin, dan /v1/payments/webhook publik untuk provider pembayaran. Peta penuh beserta auth dan validasinya dirakit utuh di modul peta API e-commerce di akhir Roadmap 2.
Hands-on Router Produk
Bangun router produk yang bisa dijalankan lokal
Sekarang kita susun contoh minimum yang memperlihatkan chi.NewRouter, method route, chi.URLParam, grouping, mounting, NotFound, dan http.ListenAndServe.
Jalankan go get github.com/go-chi/chi/v5@v5.3.0 dari root project.
Letakkan route produk di internal/product/handler.go agar domain produk tidak bercampur dengan root router.
Pakai internal/router/router.go dari section sebelumnya untuk middleware global, versioning, dan mount.
Gunakan go run ./cmd/api, lalu coba endpoint dengan curl.
internal/product/handler.gopackage product import ( "encoding/json" "fmt" "net/http" "strconv" "github.com/go-chi/chi/v5" ) type Handler struct{} type Product struct { ID int64 `json:"id"` Name string `json:"name"` Slug string `json:"slug"` Category string `json:"category"` PriceRupiah int64 `json:"price"` Stock int `json:"stock"` Status string `json:"status"` } type CreateProductRequest struct { Name string `json:"name"` Slug string `json:"slug"` Category string `json:"category"` PriceRupiah int64 `json:"price"` Stock int `json:"stock"` } type PatchProductRequest struct { Name *string `json:"name,omitempty"` PriceRupiah *int64 `json:"price,omitempty"` Stock *int `json:"stock,omitempty"` Status *string `json:"status,omitempty"` } func NewHandler() *Handler { return &Handler{} } func (h *Handler) Routes() chi.Router { r := chi.NewRouter() r.Get("/", h.List) r.Post("/", h.Create) r.Get("/{id}", h.GetByID) r.Put("/{id}", h.Replace) r.Patch("/{id}", h.UpdatePartial) r.Delete("/{id}", h.Delete) return r } func (h *Handler) List(w http.ResponseWriter, r *http.Request) { products := []Product{ {ID: 1, Name: "Gentle Low pH Cleanser", Slug: "gentle-low-ph-cleanser", Category: "cleanser", PriceRupiah: 129000, Stock: 24, Status: "active"}, {ID: 2, Name: "Niacinamide Barrier Serum", Slug: "niacinamide-barrier-serum", Category: "serum", PriceRupiah: 179000, Stock: 18, Status: "active"}, } writeJSON(w, http.StatusOK, products) } func (h *Handler) Create(w http.ResponseWriter, r *http.Request) { var req CreateProductRequest if err := json.NewDecoder(r.Body).Decode(&req); err != nil { writeError(w, http.StatusBadRequest, "request body harus berupa JSON produk yang valid") return } product := Product{ ID: 99, Name: req.Name, Slug: req.Slug, Category: req.Category, PriceRupiah: req.PriceRupiah, Stock: req.Stock, Status: "active", } writeJSON(w, http.StatusCreated, product) } func (h *Handler) GetByID(w http.ResponseWriter, r *http.Request) { id, err := productIDFromRequest(r) if err != nil { writeError(w, http.StatusBadRequest, err.Error()) return } product := Product{ID: id, Name: "Gentle Low pH Cleanser", Slug: "gentle-low-ph-cleanser", Category: "cleanser", PriceRupiah: 129000, Stock: 24, Status: "active"} writeJSON(w, http.StatusOK, product) } func (h *Handler) Replace(w http.ResponseWriter, r *http.Request) { id, err := productIDFromRequest(r) if err != nil { writeError(w, http.StatusBadRequest, err.Error()) return } var req CreateProductRequest if err := json.NewDecoder(r.Body).Decode(&req); err != nil { writeError(w, http.StatusBadRequest, "request body harus berupa JSON produk yang valid") return } product := Product{ID: id, Name: req.Name, Slug: req.Slug, Category: req.Category, PriceRupiah: req.PriceRupiah, Stock: req.Stock, Status: "active"} writeJSON(w, http.StatusOK, product) } func (h *Handler) UpdatePartial(w http.ResponseWriter, r *http.Request) { id, err := productIDFromRequest(r) if err != nil { writeError(w, http.StatusBadRequest, err.Error()) return } var req PatchProductRequest if err := json.NewDecoder(r.Body).Decode(&req); err != nil { writeError(w, http.StatusBadRequest, "request body harus berupa JSON produk yang valid") return } writeJSON(w, http.StatusOK, map[string]any{"id": id, "patched": req}) } func (h *Handler) Delete(w http.ResponseWriter, r *http.Request) { if _, err := productIDFromRequest(r); err != nil { writeError(w, http.StatusBadRequest, err.Error()) return } w.WriteHeader(http.StatusNoContent) } func productIDFromRequest(r *http.Request) (int64, error) { rawID := chi.URLParam(r, "id") id, err := strconv.ParseInt(rawID, 10, 64) if err != nil || id <= 0 { return 0, fmt.Errorf("product id tidak valid: %q", rawID) } return id, nil } func writeJSON(w http.ResponseWriter, status int, payload any) { w.Header().Set("Content-Type", "application/json") w.WriteHeader(status) _ = json.NewEncoder(w).Encode(payload) } func writeError(w http.ResponseWriter, status int, message string) { writeJSON(w, status, map[string]string{"error": message}) }
cmd/api/main.gopackage main import ( "encoding/json" "log" "net/http" "github.com/go-chi/chi/v5" "github.com/kamu/skincare-backend/internal/product" "github.com/kamu/skincare-backend/internal/router" ) func main() { productHandler := product.NewHandler() r := router.New( productHandler.Routes(), placeholderRoutes("cart"), placeholderRoutes("orders"), placeholderRoutes("payments"), ) log.Println("listening on :8080") if err := http.ListenAndServe(":8080", r); err != nil { log.Fatal(err) } } func placeholderRoutes(name string) http.Handler { r := chi.NewRouter() r.Get("/", func(w http.ResponseWriter, r *http.Request) { w.Header().Set("Content-Type", "application/json") w.WriteHeader(http.StatusOK) _ = json.NewEncoder(w).Encode(map[string]string{ "module": name, "status": "belum diimplementasi", }) }) return r }
Terminalgo get github.com/go-chi/chi/v5@v5.3.0 go run ./cmd/api
Coba health check, lalu daftar dan detail produk.
Terminalcurl -i http://localhost:8080/healthz curl -i http://localhost:8080/v1/products curl -i http://localhost:8080/v1/products/1
Coba membuat produk baru. Perhatikan field JSON-nya bernama price, walau di Go tipenya PriceRupiah int64.
Terminalcurl -i -X POST http://localhost:8080/v1/products \ -H 'Content-Type: application/json' \ -d '{"name":"Hydrating Toner","slug":"hydrating-toner","category":"toner","price":149000,"stock":30}'
Coba juga route yang tidak ada dan method yang salah untuk melihat 404 dan 405 dalam bentuk JSON.
Terminalcurl -i http://localhost:8080/v1/tidak-ada curl -i -X DELETE http://localhost:8080/v1/products
Fokus modul ini adalah routing. Data produk masih hardcoded di handler. Repository PostgreSQL dengan pgxpool masuk di Roadmap 3, lalu service layer yang rapi masuk di Roadmap 4.
Jebakan Umum
Hal kecil yang sering membuat route sulit dirawat
chi sederhana, tetapi pola yang salah bisa membuat API cepat berantakan.
Business logic di router
Router jangan menghitung total order, validasi stok, atau memanggil banyak query. Router cukup menyusun middleware dan route.
Path parameter tidak divalidasi
chi.URLParam mengembalikan string. Parse dan validasi sebelum masuk service atau repository.
Prefix versi tersebar
Jangan menulis /v1 di tiap package domain. Letakkan versioning di root router lewat Route("/v1", ...).
Mount path duplikat
Hindari mount dua subrouter pada path yang sama. Root router harus jadi satu sumber kebenaran daftar mount.
r.Use setelah route
Mendaftarkan middleware sesudah route memicu panic saat startup. Taruh semua r.Use di paling atas blok router.
404 dan 405 teks polos
Tanpa NotFound dan MethodNotAllowed kustom, client React gagal response.json(). Balas JSON yang konsisten.
RealIP sebagai default
middleware.RealIP deprecated dan rawan spoofing. Pakai middleware client-IP eksplisit sesuai infrastruktur.
Webhook di balik auth login
/v1/payments/webhook dipanggil server provider, bukan user. Biarkan publik, amankan dengan verifikasi signature.
Router dibuat sekali lalu melayani banyak request secara concurrent. Jangan menaruh data user login, cart sementara, atau payload request sebagai field router. State request hidup di *http.Request dan context, bukan di object router.
Mulai dari sini, tiap domain punya handler dan Routes() sendiri. Saat auth masuk di modul Autentikasi, kita cukup membungkus group yang perlu dilindungi dengan r.Group(func(r chi.Router) { r.Use(authMiddleware); ... }), tanpa menyentuh route publik seperti katalog dan webhook.
Ringkasan & Poin Penting
Routing dengan chi membuat API Go tetap dekat dengan net/http, tetapi jauh lebih rapi untuk proyek REST yang tumbuh.
Yang Wajib Menempel
chi.NewRouter()membuat router yang tetap memenuhihttp.Handler, jadihttptestdanListenAndServejalan tanpa perubahan.r.Get,r.Post,r.Put,r.Patch, danr.Deletemembuat mapping REST eksplisit, dengan handler bentuk standarnet/http.chi.URLParam(r, "id")mengambil path parameter sebagai string; selalu parse keint64dan validasi sebelum dipakai.r.Route("/v1", ...)untuk grouping prefix,r.Mount("/products", router)untuk memasang subrouter domain dari package lain.r.Usemenyusun middleware chain global (RequestID, Logger, Recoverer, Timeout) dengan urutan luar-ke-dalam, dan harus didaftarkan sebelum route.r.Groupmenerapkan middleware ke sekumpulan route tanpa prefix,r.Withke satu route; pakai untuk memisahkan route publik, customer, dan admin.- Hindari
middleware.RealIP(deprecated, rawan spoofing); pasangNotFounddanMethodNotAllowedkustom agar 404 dan 405 tetap JSON. - Di proyek skincare, root router di
internal/routermemetakan/v1/products(publik),/v1/cartdan/v1/checkout(customer),/v1/admin/products(admin), serta/v1/payments/webhook(publik).
Modul ini memindahkan kita dari handler tunggal ke struktur API yang siap tumbuh. Di modul berikutnya, Desain Request & Response, kita merapikan bentuk JSON menjadi envelope yang konsisten (data, meta, dan error terstruktur), lalu lanjut ke middleware serius, validasi, autentikasi, dan akhirnya peta API e-commerce yang merakit semuanya.
Progress disimpan lokal di browser ini.