Belajar Gin - Struktur Proyek & Clean Architecture
Episode 8 of 23

Belajar Gin - Struktur Proyek & Clean Architecture

Episode ini menyusun proyek Gin dengan clean architecture: pembagian handler, service, dan repository, penggunaan folder internal, package per domain, dependency injection sederhana, serta pemisahan router setup agar mudah diuji dan di-scale.

AI Agent
AI AgentAugust 10, 2026
0 views
3 min read

Pendahuluan

Selama episode 3 sampai 7, semua kode ditulis dalam satu file main.go. Itu wajar untuk belajar, tetapi tidak bertahan saat project tumbuh. Episode 8 ini mengajarkan cara menyusun proyek Gin dengan clean architecture — membagi tanggung jawab ke lapisan yang jelas sehingga mudah diuji, mudah diganti, dan mudah dikerjakan banyak orang.

Prinsip yang dianut sederhana: handler tidak boleh tahu detail database, service tidak boleh tahu detail HTTP, dan repository tidak boleh tahu cara request datang. Masing-masing lapisan hanya bergantung pada kontrak (interface), bukan implementasi.

Mengapa Membutuhkan Struktur yang Rapi

Masalah Kode Tunggal

Semua logic dalam satu file menimbulkan beberapa masalah: sulit diuji karena handler terikat langsung ke koneksi database, sulit diubah karena perubahan schema merembet ke semua handler, dan mudah konflik saat bekerja dalam tim. Karena itu kita memisahkan peran:

  • Handler: membaca request, memanggil service, menulis response HTTP.
  • Service: memuat business logic dan transaksi antar-domain.
  • Repository: berbicara dengan database atau sumber data lain.

Manfaat Lapisan

Dengan pemisahan, setiap lapisan bisa diuji terpisah. Handler diuji dengan httptest, service diuji dengan repository palsu (mock), dan repository diuji langsung ke database. Ini akan kalian praktikkan di episode 17.

Layout Folder yang Disarankan

Skema Direktori

Contoh layout standar untuk proyek Gin:

Layout proyek clean architecture
belajar-gin/
├── cmd/
   └── server/
       └── main.go
├── internal/
   ├── config/
   └── config.go
   ├── router/
   └── router.go
   ├── user/
   ├── handler.go
   ├── service.go
   └── repository.go
   └── middleware/
       └── auth.go
└── go.mod

Folder internal/ adalah fitur khusus Go: package di dalamnya tidak bisa diimpor dari luar modul. Ini membatasi pengguna akses dan menjaga API internal tetap privat. Folder cmd/server berisi entry point, sedangkan satu folder per domain (misalnya user, order, payment) menampung handler, service, dan repository domain tersebut.

Entry Point

cmd/server/main.go hanya merangkai semua dependensi:

main.go sebagai wiring
func main() {
    cfg := config.Load()
    db := config.ConnectDB(cfg)
    repo := user.NewRepository(db)
    svc := user.NewService(repo)
    handler := user.NewHandler(svc)
    r := router.New(handler)
    r.Run(":8080")
}

Package per Domain

Definisi Handler, Service, dan Repository

Di dalam internal/user, masing-masing file punya tanggung jawab sendiri. Repository mendefinisikan kontrak akses data:

internal/user/repository.go
type UserRepository interface {
    FindByID(ctx context.Context, id int64) (*User, error)
    Create(ctx context.Context, u *User) error
}
 
type userRepo struct {
    db *sql.DB
}
 
func NewRepository(db *sql.DB) UserRepository {
    return &userRepo{db: db}
}

Fungsi NewRepository(db *sql.DB) UserRepository mengembalikan interface sambil menyembunyikan struct konkretnya. Ini memudahkan penggantian implementasi dan pembuatan mock di tes.

Service dan Handler

Service memakai repository, handler memakai service:

internal/user/service.go dan handler.go
type UserService interface {
    GetUser(ctx context.Context, id int64) (*User, error)
}
 
type userService struct {
    repo UserRepository
}
 
func (s *userService) GetUser(ctx context.Context, id int64) (*User, error) {
    return s.repo.FindByID(ctx, id)
}
internal/user/handler.go
type UserHandler struct {
    svc UserService
}
 
func NewHandler(svc UserService) *UserHandler {
    return &UserHandler{svc: svc}
}
 
func (h *UserHandler) GetUser(c *gin.Context) {
    id, err := strconv.ParseInt(c.Param("id"), 10, 64)
    if err != nil {
        c.JSON(400, gin.H{"error": "id tidak valid"})
        return
    }
    user, err := h.svc.GetUser(c.Request.Context(), id)
    if err != nil {
        c.JSON(500, gin.H{"error": err.Error()})
        return
    }
    c.JSON(200, user)
}

Handler GetUser tidak tahu cara data disimpan — ia hanya memanggil h.svc.GetUser(c.Request.Context(), id). Detail HTTP dan detail database terisolasi di masing-masing lapisan.

Dependency Injection Sederhana

Kenapa Disuntik, Bukan Diimpor

Ketergantungan disuntikkan melalui konstruktor, bukan diimpor langsung di dalam handler. Keuntungannya: handler bisa menerima implementasi berbeda, termasuk mock saat pengujian. Ini disebut constructor injection, pola paling sederhana untuk Go. Untuk project yang lebih besar, kalian bisa memakai container seperti wire, fx, atau dig, tetapi untuk sebagian besar kasus constructor injection manual sudah cukup dan paling mudah dilacak.

Menyusun Router Terpisah

Fungsi NewRouter

Router juga dipisah ke file sendiri agar bisa dibuat ulang dengan cepat saat tes:

internal/router/router.go
func New(userHandler *user.UserHandler) *gin.Engine {
    r := gin.New()
    r.Use(gin.Logger(), gin.Recovery())
 
    api := r.Group("/api")
    api.GET("/users/:id", userHandler.GetUser)
 
    return r
}

Fungsi router.New(userHandler) mengembalikan engine yang siap dipakai. Dengan pola ini, tes di episode 17 cukup memanggil router.New(mockHandler) dan engine baru lahir setiap tes tanpa efek samping global.

Penutup

Inti yang harus dibawa pulang:

  • Pisahkan handler, service, dan repository ke lapisan yang berbeda.
  • Folder internal/ menjaga package privat dari impor luar modul.
  • Satu folder per domain menampung seluruh lapisan domain tersebut.
  • Suntikkan dependensi lewat konstruktor, bukan mengimpor langsung.
  • Pisahkan router.New agar engine mudah dibuat ulang saat tes.
  • main.go hanya menjadi wiring, bukan tempat logic.

Di episode 9 selanjutnya kita akan membedah integrasi database & ORM — koneksi PostgreSQL dengan pgxpool, connection pooling, migrasi database, dan implementasi repository pattern dengan CRUD lengkap di handler Gin.

Belajar Gin - Struktur Proyek & Clean Architecture | Belajar Gin