Web Artisan
Beranda

Progress belajar

Modul 14 dari 73

0% 0/73 modul selesai

Setelah selesai, tandai modul ini agar progres kursus tetap rapi.

Progress disimpan lokal di browser ini.

Roadmap 2 · Web API

Membangun HTTP Handler
dengan net/http

Di modul ini kita membuat API produk skincare memakai standard library dulu, supaya saat masuk chi kamu tahu fondasi yang sedang dibungkus.

Bahasa: Go 1.26Roadmap 2~60 menit baca
01

Dari Route Handler ke HTTP Handler

Sebelum chi, pahami kontrak HTTP bawaan Go

Kalau kamu pernah menulis Express.js, Laravel controller, atau Next.js route handler, konsep handler di Go punya tujuan yang sama: menerima request, memutuskan respons, lalu mengembalikannya ke client.

Di Express.js kamu biasa menulis fungsi seperti (req, res) => res.json(data). Di Go bentuknya mirip secara mental, tetapi berbeda secara kontrak: handler menerima http.ResponseWriter dan *http.Request, lalu menulis respons melalui writer.

Express.js route handler
  • req membawa method, path, query, header, dan body.
  • res punya helper seperti status() dan json().
  • Framework memberi banyak convenience sejak awal.
Go net/http handler
  • *http.Request membawa method, URL, header, body, dan context.
  • http.ResponseWriter dipakai untuk header, status code, dan body.
  • Standard library kecil dan eksplisit, helper perlu kamu buat sendiri.
🌉Jembatan: handler Go bukan controller Laravel

Di Laravel, controller sering langsung punya akses ke request object, validation helper, response helper, middleware group, dan dependency container. Di net/http, fondasinya sengaja minimal, sehingga kamu melihat jelas batas antara protokol HTTP, routing, validasi, dan business logic.

Tujuan modul ini bukan membuat arsitektur final. Tujuannya adalah membuat kamu nyaman dengan bahan mentah yang akan dipakai lagi saat memakai chi, handler testing, middleware, context, dan repository PostgreSQL.

02

http.Handler dan HandlerFunc

Kontrak paling kecil untuk menerima HTTP request

Di Go, server HTTP tidak peduli handler kamu berupa struct, function, atau router besar, selama nilainya memenuhi interface http.Handler.

http.Handler

Interface dari package net/http yang memiliki satu method, yaitu ServeHTTP(w http.ResponseWriter, r *http.Request).

Bentuk sederhananya seperti ini.

kontrak-handler.go
type Handler interface { ServeHTTP(ResponseWriter, *Request) }

Karena interface Go dipenuhi secara implicit, struct apa pun yang punya method ServeHTTP otomatis bisa menjadi handler.

internal/product/handler_struct.go
type ProductHandler struct { // nanti diisi service atau repository } func (h ProductHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { w.WriteHeader(http.StatusOK) w.Write([]byte("product handler")) }

Untuk kasus sederhana, kamu tidak perlu membuat struct. http.HandlerFunc adalah adapter yang mengubah fungsi biasa menjadi http.Handler.

handlerfunc.go
func healthHandler(w http.ResponseWriter, r *http.Request) { w.WriteHeader(http.StatusOK) w.Write([]byte("ok")) } mux.HandleFunc("GET /health", healthHandler)
💡Idiom praktis

Gunakan function handler untuk endpoint kecil atau modul awal. Gunakan struct handler ketika handler butuh dependency seperti service, logger, validator, atau konfigurasi.

Yang membuat kontrak ini kuat: http.ServeMux sendiri juga memenuhi http.Handler. Jadi router, middleware, dan handler kamu adalah jenis nilai yang sama, dan bisa saling dibungkus tanpa batas.

flowchart LR
  REQ([HTTP Request]) --> SRV["http.Server"]
  SRV --> MUX["ServeMux<br/>(juga http.Handler)"]
  MUX -->|cocokkan pattern route| H["HandlerFunc / struct<br/>ServeHTTP(w, r)"]
  H --> RESP([HTTP Response])

Gambar 1. Semua komponen di rantai ini adalah http.Handler, sehingga middleware nanti cukup membungkus handler dan mengembalikan handler lagi.

http.Handler

Kontrak abstrak. Cocok untuk middleware, router, dan dependency yang bisa dibungkus.

http.HandlerFunc

Convenience untuk fungsi biasa dengan signature handler.

ServeHTTP

Method yang dipanggil server untuk setiap request yang match route.

03

Membaca Request dan Menulis Response

Dua objek utama dalam setiap handler

Semua handler Go berputar di sekitar dua parameter: w untuk menulis respons dan r untuk membaca request.

*http.Request menyimpan data yang datang dari client. Untuk backend API, bagian yang paling sering kamu baca adalah r.Method, r.URL.Path, r.URL.Query(), r.Header, r.Body, dan r.Context().

membaca-request.go
func debugHandler(w http.ResponseWriter, r *http.Request) { method := r.Method path := r.URL.Path category := r.URL.Query().Get("category") requestID := r.Header.Get("X-Request-ID") _ = method _ = path _ = category _ = requestID w.WriteHeader(http.StatusNoContent) }

http.ResponseWriter adalah interface dengan tiga method: Header() untuk mengubah header, WriteHeader(status) untuk mengirim status code, dan Write([]byte) untuk menulis body. Urutan yang aman adalah set header dulu, tulis status code, lalu tulis body.

menulis-response.go
func helloHandler(w http.ResponseWriter, r *http.Request) { w.Header().Set("Content-Type", "text/plain; charset=utf-8") w.WriteHeader(http.StatusOK) w.Write([]byte("hello from Go API")) }

Diagram berikut menjelaskan kenapa urutan tadi penting. Begitu status terkirim, header membeku dan tidak bisa diubah lagi.

stateDiagram-v2
  [*] --> HeaderTerbuka
  HeaderTerbuka --> HeaderTerbuka: w.Header().Set(...)
  HeaderTerbuka --> StatusTerkirim: w.WriteHeader(status)
  StatusTerkirim --> BodyMengalir: w.Write(...)
  BodyMengalir --> BodyMengalir: w.Write(...) lagi
  BodyMengalir --> [*]
  note right of StatusTerkirim: Header beku di sini.<br/>Set header setelahnya diabaikan.

Gambar 2. Siklus menulis response. Set semua header sebelum WriteHeader, karena setelah status terkirim header tidak berpengaruh lagi.

⚠️Urutan penting

Setelah WriteHeader atau Write dipanggil, sebagian besar perubahan header tidak akan berpengaruh. Anggap saja respons sudah mulai dikirim ke client.

Dalam proyek online shop skincare, handler produk akan membaca query seperti category=serum, header seperti Authorization, dan body JSON saat admin membuat produk baru.

GET /v1/products?category=serum Membaca query parameter untuk filter katalog
POST /v1/products Membaca request body JSON untuk membuat produk baru
GET /v1/products/{id} Membaca path parameter untuk detail produk
04

JSON Response dan Request Body

Cara paling umum React frontend bicara dengan Go API

React frontend biasanya mengirim dan menerima JSON. Di Go, package encoding/json sudah cukup untuk tahap awal.

Untuk respons JSON, set Content-Type, status code, lalu pakai json.NewEncoder(w).Encode(payload).

json-response.go
type ProductResponse struct { ID int64 `json:"id"` Name string `json:"name"` PriceRupiah int64 `json:"price"` } func productHandler(w http.ResponseWriter, r *http.Request) { product := ProductResponse{ ID: 1, Name: "Niacinamide Serum", PriceRupiah: 189000, } w.Header().Set("Content-Type", "application/json") w.WriteHeader(http.StatusOK) json.NewEncoder(w).Encode(product) }

Untuk request JSON, buat struct request, lalu decode dari r.Body.

json-request.go
type CreateProductRequest struct { Name string `json:"name"` Category string `json:"category"` PriceRupiah int64 `json:"price"` Stock int `json:"stock"` } func createProductHandler(w http.ResponseWriter, r *http.Request) { var req CreateProductRequest if err := json.NewDecoder(r.Body).Decode(&req); err != nil { http.Error(w, "invalid JSON", http.StatusBadRequest) return } w.WriteHeader(http.StatusCreated) }
🌉Jembatan: dari `response.json()` ke `json.Encoder`

Di JavaScript, response.json() membaca body response menjadi object. Di Go server, json.NewEncoder(w).Encode(value) melakukan kebalikannya: mengubah value Go menjadi JSON lalu menulisnya ke response body.

📝Kenapa `price` disimpan sebagai `int64`

Rupiah tidak punya pecahan sen yang dipakai sehari-hari, jadi kita simpan harga sebagai bilangan bulat rupiah (189000 berarti Rp 189.000). Menghindari float64 untuk uang adalah praktik standar agar tidak ada galat pembulatan.

Ada dua helper kecil yang hampir selalu kita buat agar handler tidak mengulang boilerplate.

response.go
func writeJSON(w http.ResponseWriter, status int, payload any) { w.Header().Set("Content-Type", "application/json") w.WriteHeader(status) if err := json.NewEncoder(w).Encode(payload); err != nil { slog.Error("encode response", "error", err) } } func writeError(w http.ResponseWriter, status int, message string) { writeJSON(w, status, map[string]string{"error": message}) }
💡Batas body request

Untuk endpoint publik, batasi ukuran r.Body dengan http.MaxBytesReader. Tanpa batas, client nakal bisa mengirim body sangat besar dan menghabiskan resource server.

decode-dengan-batas.go
r.Body = http.MaxBytesReader(w, r.Body, 1<<20) // 1 MB decoder := json.NewDecoder(r.Body) decoder.DisallowUnknownFields() if err := decoder.Decode(&req); err != nil { writeError(w, http.StatusBadRequest, "request body tidak valid") return }
🧩Saat batas terlampaui

Bila body melewati batas, Decode mengembalikan error bertipe *http.MaxBytesError. Untuk respons yang lebih jujur, kamu bisa memeriksanya dengan errors.As lalu membalas 413 Request Entity Too Large, bukan sekadar 400.

05

Status Code sebagai Kontrak API

Client React bergantung pada status code yang konsisten

Status code bukan kosmetik. Ia adalah kontrak yang membuat frontend, mobile app, worker, dan payment provider tahu hasil request tanpa menebak isi body.

Di Go, status code dikirim dengan w.WriteHeader(code). Bila kamu langsung memanggil w.Write(...) tanpa WriteHeader, Go akan mengirim 200 OK secara implicit.

200 OK

Request sukses dan respons membawa data, misalnya daftar produk.

201 Created

Resource berhasil dibuat, misalnya produk admin atau order checkout.

400 Bad Request

JSON rusak, parameter invalid, atau format request salah.

404 Not Found

Resource tidak ada, misalnya produk dengan ID tertentu tidak ditemukan.

422 Unprocessable Entity

JSON valid, tetapi aturan bisnis gagal, misalnya harga produk 0.

500 Internal Server Error

Error yang tidak bisa dipulihkan di server. Detail internal jangan dibocorkan ke client.

status-code.go
func createProductHandler(w http.ResponseWriter, r *http.Request) { // setelah produk berhasil dibuat w.Header().Set("Content-Type", "application/json") w.WriteHeader(http.StatusCreated) json.NewEncoder(w).Encode(product) }
⚠️Jangan tulis status dua kali

Dalam satu response normal, kamu hanya punya satu status akhir. Setelah WriteHeader(http.StatusCreated), jangan mencoba mengubahnya menjadi 400 di bawahnya.

Pola guard clause dari React sangat cocok di Go. Validasi error lebih baik return lebih awal.

guard-clause.go
if err := decoder.Decode(&req); err != nil { writeError(w, http.StatusBadRequest, "JSON tidak valid") return } if req.PriceRupiah <= 0 { writeError(w, http.StatusUnprocessableEntity, "harga harus lebih dari 0") return }
06

Contoh Lengkap Handler Produk

Satu file lengkap tanpa chi, fokus ke net/http

Contoh ini sengaja memakai in-memory store agar perhatian kita tetap pada handler HTTP, bukan database.

Struktur latihan modul ini
  • skincare-api/
  • go.mod
  • handler.go contoh lengkap modul ini
handler.go
package main import ( "encoding/json" "errors" "log/slog" "net/http" "os" "sort" "strconv" "strings" "sync" ) type Product struct { ID int64 `json:"id"` Name string `json:"name"` Category string `json:"category"` PriceRupiah int64 `json:"price"` Stock int `json:"stock"` Description string `json:"description,omitempty"` Status string `json:"status"` } type CreateProductRequest struct { Name string `json:"name"` Category string `json:"category"` PriceRupiah int64 `json:"price"` Stock int `json:"stock"` Description string `json:"description"` } type ErrorResponse struct { Error string `json:"error"` } type ProductStore struct { mu sync.RWMutex nextID int64 products map[int64]Product } func main() { store := NewProductStore() mux := http.NewServeMux() mux.HandleFunc("GET /health", healthHandler) mux.HandleFunc("GET /v1/products", store.ListProducts) mux.HandleFunc("POST /v1/products", store.CreateProduct) mux.HandleFunc("GET /v1/products/{id}", store.GetProduct) addr := ":8080" slog.Info("server listening", "addr", addr) if err := http.ListenAndServe(addr, mux); err != nil { slog.Error("server stopped", "error", err) os.Exit(1) } } func NewProductStore() *ProductStore { return &ProductStore{ nextID: 4, products: map[int64]Product{ 1: {ID: 1, Name: "Hydrating Cleanser", Category: "cleanser", PriceRupiah: 129000, Stock: 40, Status: "active"}, 2: {ID: 2, Name: "Niacinamide Serum", Category: "serum", PriceRupiah: 189000, Stock: 22, Status: "active"}, 3: {ID: 3, Name: "Daily Sunscreen SPF 50", Category: "sunscreen", PriceRupiah: 159000, Stock: 35, Status: "active"}, }, } } func healthHandler(w http.ResponseWriter, r *http.Request) { writeJSON(w, http.StatusOK, map[string]string{"status": "ok"}) } func (s *ProductStore) ListProducts(w http.ResponseWriter, r *http.Request) { category := strings.TrimSpace(r.URL.Query().Get("category")) keyword := strings.ToLower(strings.TrimSpace(r.URL.Query().Get("q"))) s.mu.RLock() products := make([]Product, 0, len(s.products)) for _, product := range s.products { if category != "" && product.Category != category { continue } if keyword != "" && !strings.Contains(strings.ToLower(product.Name), keyword) { continue } products = append(products, product) } s.mu.RUnlock() // Iterasi map di Go acak. Urutkan agar urutan list stabil untuk frontend. sort.Slice(products, func(i, j int) bool { return products[i].ID < products[j].ID }) writeJSON(w, http.StatusOK, products) } func (s *ProductStore) GetProduct(w http.ResponseWriter, r *http.Request) { id, err := parseID(r.PathValue("id")) if err != nil { writeError(w, http.StatusBadRequest, "product id harus berupa angka positif") return } s.mu.RLock() product, ok := s.products[id] s.mu.RUnlock() if !ok { writeError(w, http.StatusNotFound, "produk tidak ditemukan") return } writeJSON(w, http.StatusOK, product) } func (s *ProductStore) CreateProduct(w http.ResponseWriter, r *http.Request) { r.Body = http.MaxBytesReader(w, r.Body, 1<<20) var req CreateProductRequest decoder := json.NewDecoder(r.Body) decoder.DisallowUnknownFields() if err := decoder.Decode(&req); err != nil { writeError(w, http.StatusBadRequest, "request body harus berupa JSON produk yang valid") return } if err := validateCreateProduct(req); err != nil { writeError(w, http.StatusUnprocessableEntity, err.Error()) return } s.mu.Lock() product := Product{ ID: s.nextID, Name: strings.TrimSpace(req.Name), Category: strings.TrimSpace(req.Category), PriceRupiah: req.PriceRupiah, Stock: req.Stock, Description: strings.TrimSpace(req.Description), Status: "active", } s.products[product.ID] = product s.nextID++ s.mu.Unlock() writeJSON(w, http.StatusCreated, product) } func validateCreateProduct(req CreateProductRequest) error { if strings.TrimSpace(req.Name) == "" { return errors.New("nama produk wajib diisi") } if strings.TrimSpace(req.Category) == "" { return errors.New("kategori produk wajib diisi") } if req.PriceRupiah <= 0 { return errors.New("harga produk harus lebih dari 0") } if req.Stock < 0 { return errors.New("stok produk tidak boleh negatif") } return nil } func parseID(raw string) (int64, error) { id, err := strconv.ParseInt(raw, 10, 64) if err != nil || id <= 0 { return 0, errors.New("invalid id") } return id, nil } func writeJSON(w http.ResponseWriter, status int, payload any) { w.Header().Set("Content-Type", "application/json") w.WriteHeader(status) if err := json.NewEncoder(w).Encode(payload); err != nil { slog.Error("encode response", "error", err) } } func writeError(w http.ResponseWriter, status int, message string) { writeJSON(w, status, ErrorResponse{Error: message}) }

Diagram berikut menunjukkan alur satu request dari React app sampai respons JSON kembali.

sequenceDiagram
  participant FE as React Frontend
  participant MUX as net/http ServeMux
  participant H as Product Handler
  participant S as In-memory Store
  FE->>MUX: POST /v1/products dengan JSON
  MUX->>H: Panggil CreateProduct(w, r)
  H->>H: Decode JSON dari r.Body
  H->>H: Validasi input produk
  H->>S: Simpan product baru
  S-->>H: Product dengan ID
  H-->>FE: 201 Created application/json

Gambar 3. Alur request di modul ini masih tanpa database. Di Roadmap 3, store akan diganti repository PostgreSQL dengan pgx.

📝Tentang `sync.RWMutex` di contoh

Server HTTP Go memproses banyak request secara concurrent. Karena contoh memakai map in-memory yang bisa dibaca dan ditulis beberapa request, kita memakai mutex agar aman. Saat pindah ke PostgreSQL, konsistensi data akan dikelola oleh database dan transaksi.

⚠️Urutan iterasi map tidak dijamin

Berbeda dengan object JavaScript yang menjaga urutan insert, iterasi map di Go sengaja diacak. Untuk endpoint list, selalu urutkan hasilnya (di sini dengan sort.Slice) supaya frontend menerima urutan yang stabil.

07

ServeMux dan ListenAndServe

Router minimal dan cara menjalankan server

http.ServeMux adalah router bawaan standard library. Ia menerima pattern route, lalu memilih handler yang sesuai.

Sejak Go 1.22, ServeMux mendukung pattern yang lebih ekspresif, termasuk method dan wildcard path. Itu sebabnya contoh kita bisa menulis route seperti ini.

routes.go
mux := http.NewServeMux() mux.HandleFunc("GET /health", healthHandler) mux.HandleFunc("GET /v1/products", store.ListProducts) mux.HandleFunc("POST /v1/products", store.CreateProduct) mux.HandleFunc("GET /v1/products/{id}", store.GetProduct)

Nilai wildcard bisa dibaca dengan r.PathValue("id").

path-value.go
func (s *ProductStore) GetProduct(w http.ResponseWriter, r *http.Request) { idRaw := r.PathValue("id") // parse idRaw ke int64 }

http.ListenAndServe menjalankan server pada address tertentu dan memakai handler yang kamu berikan.

listen.go
addr := ":8080" if err := http.ListenAndServe(addr, mux); err != nil { slog.Error("server stopped", "error", err) os.Exit(1) }
ServeMux

Multiplexer HTTP bawaan Go yang mencocokkan request masuk ke handler berdasarkan pattern route.

ListenAndServe

Fungsi yang membuka TCP listener, menerima koneksi HTTP, lalu meneruskan request ke handler.

🧭Pattern yang lebih spesifik menang

Bila dua pattern cocok, ServeMux memilih yang paling spesifik. GET /v1/products/{id} menang atas GET /v1/products/ untuk path /v1/products/2, jadi kamu tidak perlu mengurutkan route secara manual seperti di beberapa router lain.

⚠️Jangan terlalu nyaman dengan `nil` handler

http.ListenAndServe(":8080", nil) memakai http.DefaultServeMux, yaitu mux global. Untuk proyek serius, buat http.NewServeMux() sendiri agar route tidak tersebar lewat global state.

08

Kenapa Nanti Tetap Pakai chi?

Standard library cukup kuat, tetapi proyek nyata butuh ergonomi lebih

Pertanyaan bagus untuk Go modern: kalau ServeMux sudah punya method pattern dan wildcard, kenapa masih belajar chi?

Jawabannya bukan karena net/http buruk. Justru chi dibangun di atas kontrak http.Handler, sehingga kamu tetap memakai fondasi yang sama. chi membantu saat route makin banyak, middleware makin serius, dan struktur API perlu rapi.

Middleware ergonomis

Logging, recoverer, timeout, auth, request ID, CORS, dan rate limit lebih mudah dirangkai per group route.

Route grouping

API versi /v1, admin routes, public catalog, cart, checkout, dan webhook bisa dikelompokkan dengan jelas.

Integrasi komunitas

Banyak middleware dan contoh production yang langsung memakai chi di atas net/http.

Arsitektur modular

Setiap module domain bisa punya function Routes() sendiri dan dipasang ke router utama.

ServeMux modern
  • Cukup untuk API kecil dan belajar fondasi.
  • Sudah mendukung method pattern dan wildcard path.
  • Middleware bisa dibuat, tetapi grouping dan composition lebih manual.
chi v5
  • Cocok untuk API production yang route dan middleware-nya tumbuh.
  • Group, mount, middleware stack, dan URL params terasa lebih nyaman.
  • Tetap compatible dengan http.Handler, http.HandlerFunc, dan httptest.
💡Cara berpikir yang benar

Belajar net/http dulu membuat chi terasa seperti alat bantu, bukan sulap. Saat nanti ada bug middleware atau test handler, kamu tahu kontrak aslinya tetap http.Handler.

09

Hands-on: Jalankan API Produk

Latihan cepat sebelum masuk router chi

Sekarang jalankan contoh handler produk dan coba request dari terminal.

Buat folder proyek

Gunakan module kecil khusus latihan agar tidak bercampur dengan modul lain.

Salin handler.go

Letakkan file contoh lengkap dari section sebelumnya di root folder latihan.

Jalankan server

Pakai go run ., lalu biarkan terminal tetap terbuka.

Coba endpoint

Pakai curl dari terminal lain untuk melihat response JSON dan status code.

Terminal
mkdir skincare-api cd skincare-api go mod init github.com/kamu/skincare-backend # salin handler.go ke folder ini go run .

Coba health check.

Terminal
curl -i http://localhost:8080/health

Coba daftar produk dengan filter query.

Terminal
curl -i "http://localhost:8080/v1/products?category=serum"

Coba detail produk dengan path parameter.

Terminal
curl -i http://localhost:8080/v1/products/2

Coba membuat produk baru.

Terminal
curl -i -X POST http://localhost:8080/v1/products \ -H "Content-Type: application/json" \ -d '{"name":"Barrier Repair Moisturizer","category":"moisturizer","price":219000,"stock":18,"description":"Krim pelembap untuk skin barrier"}'
📝Yang perlu kamu amati

Perhatikan status 201 Created, header Content-Type: application/json, dan body JSON berisi id produk baru. Itulah kontrak yang akan dipakai React frontend.

10

Jebakan Umum dari JS/PHP

Kesalahan kecil yang sering bikin handler Go membingungkan

Sebagian bug handler bukan karena Go sulit, tetapi karena kebiasaan dari Express.js atau Laravel terbawa tanpa disesuaikan.

Lupa return setelah error

Setelah writeError, handler tetap lanjut jika tidak return. Ini sering membuat response ditulis dua kali.

Header ditulis terlambat

Set Content-Type setelah WriteHeader biasanya sudah telat. Tulis header sebelum status dan body.

Menganggap JSON otomatis valid

Decode hanya parsing JSON. Validasi business rule tetap harus kamu tulis sendiri.

Membocorkan error internal

Jangan kirim pesan error database mentah ke client. Log detail di server, kirim pesan aman ke client.

Menggunakan map global tanpa proteksi

Server HTTP concurrent. Map yang ditulis banyak request perlu mutex atau diganti database.

Mengandalkan DefaultServeMux

Global mux terasa praktis di awal, tetapi menyulitkan testing dan membuat route tersebar.

Iterasi map dianggap terurut

Tidak seperti object JS, urutan iterasi map Go acak. Urutkan sebelum mengirim list ke client.

Lupa membatasi ukuran body

Tanpa http.MaxBytesReader, satu request besar bisa membebani server. Batasi sejak awal.

🌉Jembatan: Express middleware vs Go middleware

Di Express, middleware adalah function dengan next(). Di Go, middleware biasanya function yang menerima http.Handler dan mengembalikan http.Handler. Kita akan membahas pola ini sebelum masuk chi middleware.

bentuk-middleware-go.go
func logging(next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { slog.Info("request", "method", r.Method, "path", r.URL.Path) next.ServeHTTP(w, r) }) }
11

Ringkasan & Poin Penting

Sekarang kamu sudah bisa membaca dan menulis HTTP request secara langsung dengan standard library Go.

Yang Wajib Menempel

  • http.Handler adalah kontrak dasar server HTTP Go, dan http.HandlerFunc adalah adapter untuk function biasa. ServeMux pun memenuhi kontrak yang sama.
  • *http.Request dipakai untuk membaca method, URL, query, header, body, path value, dan context.
  • http.ResponseWriter dipakai untuk set header, status code, dan body response, dengan urutan header dulu baru status dan body.
  • json.NewEncoder(w).Encode(...) adalah cara praktis mengirim respons JSON, sedangkan json.NewDecoder(r.Body).Decode(&req) membaca JSON request, dilengkapi MaxBytesReader agar body tidak tak terbatas.
  • Status code adalah kontrak API, bukan detail kosmetik. Pakai 201 untuk created, 400 untuk request rusak, 422 untuk validasi bisnis, dan 404 untuk resource tidak ada.
  • Iterasi map Go acak, jadi urutkan output list secara eksplisit agar kontrak ke frontend stabil.
  • ServeMux modern sudah cukup untuk API kecil, tetapi chi akan membantu route grouping, middleware stack, dan modularisasi API production.

Untuk proyek online shop skincare, modul ini adalah fondasi endpoint produk. Di modul berikutnya, kita akan mulai merapikan routing dengan chi, lalu lanjut ke middleware, request context, dan akhirnya repository PostgreSQL dengan pgx.

Progress disimpan lokal di browser ini.