Episode ini membahas cara mengorganisasi project Go yang memakai chi agar bisa tumbuh: memisahkan handler, service, dan repository, menyusun layout internal, menerapkan dependency injection, dan mendefinisikan router sebagai fungsi yang mengembalikan http.Handler untuk kemudahan testing.

Semua route di satu main.go terasa nyaman saat aplikasi berisi sepuluh baris. Saat aplikasi menyentuh ribuan baris dengan beberapa resource, struktur menjadi penentu kelangsungan hidup project. Episode 8 menjawabnya dengan pola yang sudah teruji: handler, service, repository, dipadukan dengan layout internal dan dependency injection.
Tidak ada resep tunggal yang wajib — yang ada adalah prinsip: pemisahan tanggung jawab, arah dependensi yang jelas, dan kemudahan testing. chi tidak memaksa struktur apa pun; ia memberi kebebasan penuh untuk membangunnya.
Aliran data satu arah: handler menuju service menuju repository. Repository tidak pernah tahu tentang HTTP; handler tidak pernah menulis SQL.
type UserService interface {
GetByID(ctx context.Context, id int) (User, error)
}
type UserRepo interface {
FindByID(ctx context.Context, id int) (User, error)
}UserService dan UserRepo didefinisikan sebagai interface di sisi pemanggil — ini memudahkan mocking saat testing, seperti yang akan kita lihat di episode 17.
Susunan yang terbukti berjalan baik:
chi-service/
├── cmd/server/main.go
├── internal/
│ ├── handler/
│ ├── service/
│ └── repository/
└── go.modcmd/server/main.go hanya berisi wiring: membuat router, menyuntikkan dependency, menjalankan server. Seluruh logika ada di internal/ yang tidak bisa di-import dari luar module.
Package internal Go tidak dapat di-import oleh module lain — ini batasan kompilasi yang melindungi API internal kalian. Layout ini memisahkan antarmuka publik (router, entrypoint) dari implementasi.
Jangan biarkan handler membuat dependency sendiri. Suntikkan lewat struct:
type UserHandler struct {
svc service.UserService
}
func (h *UserHandler) GetByID(w http.ResponseWriter, req *http.Request) {
id, _ := strconv.Atoi(chi.URLParam(req, "id"))
user, err := h.svc.GetByID(req.Context(), id)
if err != nil {
http.Error(w, "user tidak ditemukan", http.StatusNotFound)
return
}
writeJSON(w, http.StatusOK, user)
}h.svc.GetByID(req.Context(), id) memanggil service yang diinjeksi. Handler tidak perlu tahu service dibuat dari mana — database asli saat produksi, mock saat testing.
Alternatif tanpa struct adalah closure:
func makeGetUser(svc service.UserService) http.HandlerFunc {
return func(w http.ResponseWriter, req *http.Request) {
id, _ := strconv.Atoi(chi.URLParam(req, "id"))
user, err := svc.GetByID(req.Context(), id)
if err != nil {
http.Error(w, "not found", http.StatusNotFound)
return
}
writeJSON(w, http.StatusOK, user)
}
}makeGetUser(svc) mengembalikan handler yang menutup akses ke svc. Pola ini paling ringkas untuk resource sederhana.
Pola yang dipakai banyak project production:
type Deps struct {
Users *handler.UserHandler
}
func Routes(deps Deps) http.Handler {
r := chi.NewRouter()
r.Use(middleware.RequestID)
r.Use(middleware.Logger)
r.Use(middleware.Recoverer)
r.Get("/users", deps.Users.List)
r.Post("/users", deps.Users.Create)
r.Get("/users/{id}", deps.Users.GetByID)
return r
}Routes(deps Deps) http.Handler membangun seluruh router dari dependency yang diberikan. main.go tinggal menyusun Deps lalu menyerahkan hasilnya ke http.ListenAndServe.
Karena Routes menerima Deps, kalian bisa membuat Deps dengan mock service dan menguji seluruh router:
deps := Deps{Users: &handler.UserHandler{
svc: mockUserService{},
}}
srv := httptest.NewServer(Routes(deps))
defer srv.Close()httptest.NewServer(Routes(deps)) menjalankan router asli dengan dependency palsu — inilah mengapa pola ini jadi standar project chi berskala besar.
main, bukan di dalam handler.Struktur yang baik membuat perubahan kecil tidak merembet ke seluruh aplikasi.
Inti yang harus dibawa pulang:
internal/ melindungi kode internal dari import luar.Routes(deps) http.Handler untuk testability penuh.Di episode 9 selanjutnya kita menyambungkan aplikasi ke database: integrasi database dan ORM — pgx dan database/sql, connection pooling, migrasi, repository pattern, serta implementasi CRUD di dalam handler.