Episode ini menyambungkan router chi ke database: pilihan antara pgx, database/sql, GORM, dan sqlc, connection pooling, serta migrasi skema. Kalian juga menerapkan repository pattern dan mengimplementasikan CRUD lengkap di dalam handler berbasis http.Handler.

HTTP service tanpa database hanyalah kalkulator. Episode 9 menghubungkan router chi ke PostgreSQL — pilihan paling umum di ekosistem Go — melalui beberapa pendekatan: driver langsung pgx, database/sql, ORM GORM, dan code generator sqlc.
Setiap pendekatan punya trade-off. Episode ini memberikan peta pemilihan yang jelas, lalu membangun satu CRUD utuh yang memakai repository pattern dari episode 8.
go get github.com/jackc/pgx/v5
go get gorm.io/gorm gorm.io/driver/postgresgo get github.com/jackc/pgx/v5 mengambil driver pgx; baris kedua mengambil GORM beserta driver PostgreSQL-nya.
Pilih pgx atau database/sql untuk kontrol penuh atas SQL dan performa maksimal. Pilih GORM untuk produktivitas tinggi dengan skema sederhana. Pilih sqlc untuk query kompleks yang ingin dijamin aman tipe dan tervalidasi saat build.
Jangan membuat koneksi baru per request — selalu pakai pool:
ctx := context.Background()
pool, err := pgxpool.New(ctx, os.Getenv("DATABASE_URL"))
if err != nil {
log.Fatal(err)
}
defer pool.Close()
pool.Config().MaxConns = 20
pool.Config().MaxConnIdleTime = 5 * time.Minutepgxpool.New(ctx, os.Getenv("DATABASE_URL")) membuat pool koneksi dari URL postgres://user:pass@host:5432/db. Pool dibagi antar goroutine — ribuan request bisa memakai koneksi terbatas dengan aman.
Kelola perubahan skema sebagai file migrasi:
migrate create -ext sql -dir migrations create_users
migrate -path migrations -database "$DATABASE_URL" upmigrate create -ext sql -dir migrations create_users membuat file up dan down kosong, lalu migrate ... up menerapkan migrasi yang belum jalan. Simpan semua file migrasi di git agar riwayat skema terdokumentasi.
File up berisi skema seperti CREATE TABLE users dengan kolom id BIGSERIAL PRIMARY KEY, name, dan email UNIQUE. Kolom id memakai BIGSERIAL sehingga database mengisi otomatis.
Repository mengemas akses data di satu tempat:
type UserRepo struct {
pool *pgxpool.Pool
}
func (repo *UserRepo) FindByID(ctx context.Context, id int) (User, error) {
var u User
err := repo.pool.QueryRow(ctx,
"SELECT id, name, email FROM users WHERE id = $1", id).
Scan(&u.ID, &u.Name, &u.Email)
return u, err
}repo.pool.QueryRow(ctx, sql, id).Scan(...) menjalankan query parameterized — placeholder $1 mencegah SQL injection, bukan string concatenation.
Ambil nilai balik langsung dari database:
func (repo *UserRepo) Create(ctx context.Context, u User) (User, error) {
err := repo.pool.QueryRow(ctx,
"INSERT INTO users (name, email) VALUES ($1, $2) RETURNING id",
u.Name, u.Email).Scan(&u.ID)
return u, err
}RETURNING id membuat INSERT mengembalikan nilai kolom yang di-generate — satu round-trip, tanpa query tambahan.
Gabungkan repository, service, dan handler menjadi satu alur:
type UserHandler struct {
repo *repository.UserRepo
}
func (h *UserHandler) List(w http.ResponseWriter, req *http.Request) {
users, err := h.repo.List(req.Context())
if err != nil {
http.Error(w, "gagal memuat users",
http.StatusInternalServerError)
return
}
writeJSON(w, http.StatusOK, users)
}
func (h *UserHandler) Create(w http.ResponseWriter, req *http.Request) {
var u User
if err := json.NewDecoder(req.Body).Decode(&u); err != nil {
http.Error(w, "body tidak valid", http.StatusBadRequest)
return
}
created, err := h.repo.Create(req.Context(), u)
if err != nil {
http.Error(w, "gagal membuat user",
http.StatusInternalServerError)
return
}
writeJSON(w, http.StatusCreated, created)
}h.repo.List(req.Context()) meneruskan context request sampai ke query database — saat klien membatalkan request, query ikut dibatalkan. Ini pola yang akan kita perdalam di episode 12.
h := &handler.UserHandler{repo: userRepo}
r.Route("/users", func(users chi.Router) {
users.Get("/", h.List)
users.Post("/", h.Create)
users.Get("/{id}", h.GetByID)
users.Put("/{id}", h.Update)
users.Delete("/{id}", h.Delete)
})users.Delete("/{id}", h.Delete) menutup siklus CRUD pada satu subrouter yang rapi — persis pola subrouter yang kita bangun di episode 5.
Inti yang harus dibawa pulang:
Di episode 10 selanjutnya kita membahas konfigurasi: configuration dan environment — variabel lingkungan dengan os.Getenv, godotenv dan Viper, config struct yang diinjeksi ke handler, serta graceful shutdown dengan signal.NotifyContext.