Pendahuluan
Routing adalah salah satu fondasi paling penting dalam membangun REST API. Bagaimana cara kita mendefinisikan endpoint, menerima parameter dari client, dan mengelompokkan route-route yang berkaitan akan sangat menentukan seberapa mudah aplikasi tersebut dikembangkan dan dipelihara di kemudian hari. Gin, sebagai salah satu web framework Golang paling populer, menyediakan sistem routing yang cepat (berbasis radix tree) sekaligus fleksibel.
Artikel ini membahas tiga hal utama dalam routing Gin: path parameter, query parameter, dan route grouping, lengkap dengan praktik penulisan yang rapi agar mudah di-maintain saat project semakin besar.
Dasar Routing di Gin
Sebelum masuk ke detail, berikut contoh routing paling sederhana di Gin:
go
package main
import "github.com/gin-gonic/gin"
func main() {
router := gin.Default()
router.GET("/ping", func(c *gin.Context) {
c.JSON(200, gin.H{"message": "pong"})
})
router.Run(":8080")
}
Gin mendukung method HTTP standar: GET, POST, PUT, PATCH, DELETE, OPTIONS, HEAD, dan juga Any() untuk menangkap semua method sekaligus.
Path Parameter
Path parameter digunakan saat sebagian dari URL bersifat dinamis, misalnya ID resource. Di Gin, path parameter ditulis dengan tanda titik dua (:).
go
router.GET("/users/:id", func(c *gin.Context) {
id := c.Param("id")
c.JSON(200, gin.H{"user_id": id})
})
Jika request masuk ke /users/42, maka c.Param("id") akan menghasilkan "42".
Multiple Path Parameter
Gin juga mendukung lebih dari satu parameter dalam satu path:
go
router.GET("/users/:userId/orders/:orderId", func(c *gin.Context) {
userID := c.Param("userId")
orderID := c.Param("orderId")
c.JSON(200, gin.H{
"user_id": userID,
"order_id": orderID,
})
})
Wildcard Parameter
Selain parameter biasa, Gin juga mendukung wildcard dengan tanda bintang (*), yang akan menangkap sisa path setelahnya, termasuk slash (/):
go
router.GET("/files/*filepath", func(c *gin.Context) {
path := c.Param("filepath")
c.JSON(200, gin.H{"path": path})
})
Request ke /files/documents/2024/report.pdf akan menghasilkan filepath bernilai /documents/2024/report.pdf. Wildcard ini cocok untuk kasus seperti file server atau proxy path.
Validasi Tipe Data Path Parameter
Karena path parameter selalu bertipe string, konversi ke tipe data lain (misalnya integer) perlu dilakukan manual dan tetap divalidasi:
go
router.GET("/products/:id", func(c *gin.Context) {
idParam := c.Param("id")
id, err := strconv.Atoi(idParam)
if err != nil {
c.JSON(400, gin.H{"message": "ID produk tidak valid"})
return
}
c.JSON(200, gin.H{"product_id": id})
})
Query Parameter
Berbeda dengan path parameter yang menjadi bagian dari struktur URL, query parameter bersifat opsional dan biasanya dipakai untuk filter, pencarian, atau pagination. Contoh: /products?category=elektronik&page=2&limit=10.
Mengambil Query Parameter Dasar
go
router.GET("/products", func(c *gin.Context) {
category := c.Query("category")
c.JSON(200, gin.H{"category": category})
})
c.Query() akan mengembalikan string kosong jika parameter tidak dikirim.
Query Parameter dengan Default Value
Untuk kasus seperti pagination, sering kali dibutuhkan nilai default jika client tidak mengirim parameter tersebut:
go
router.GET("/products", func(c *gin.Context) {
page := c.DefaultQuery("page", "1")
limit := c.DefaultQuery("limit", "10")
c.JSON(200, gin.H{
"page": page,
"limit": limit,
})
})
Binding Query Parameter ke Struct
Untuk endpoint dengan banyak query parameter, lebih rapi jika di-bind langsung ke struct menggunakan ShouldBindQuery:
go
type ProductFilter struct {
Category string `form:"category"`
MinPrice int `form:"min_price"`
MaxPrice int `form:"max_price"`
Page int `form:"page,default=1"`
Limit int `form:"limit,default=10"`
}
router.GET("/products", func(c *gin.Context) {
var filter ProductFilter
if err := c.ShouldBindQuery(&filter); err != nil {
c.JSON(400, gin.H{"message": "Query parameter tidak valid"})
return
}
c.JSON(200, filter)
})
Pendekatan ini jauh lebih mudah dibaca dan di-maintain dibanding memanggil c.Query() satu per satu, terutama saat filter yang dibutuhkan semakin banyak.
Route Grouping
Seiring bertambahnya jumlah endpoint, menulis semua route secara flat di satu file akan cepat menjadi berantakan. Di sinilah route grouping berperan — mengelompokkan route berdasarkan versi API, resource, atau kebutuhan middleware tertentu.
Grouping Berdasarkan Versi API
go
router := gin.Default()
v1 := router.Group("/api/v1")
{
v1.GET("/users", listUsers)
v1.GET("/users/:id", getUser)
v1.POST("/users", createUser)
}
v2 := router.Group("/api/v2")
{
v2.GET("/users", listUsersV2)
}
Pola ini memudahkan pengelolaan versi API tanpa harus menulis ulang prefix /api/v1 di setiap route.
Grouping Berdasarkan Resource
go
v1 := router.Group("/api/v1")
userRoutes := v1.Group("/users")
{
userRoutes.GET("", listUsers)
userRoutes.GET("/:id", getUser)
userRoutes.POST("", createUser)
userRoutes.PUT("/:id", updateUser)
userRoutes.DELETE("/:id", deleteUser)
}
productRoutes := v1.Group("/products")
{
productRoutes.GET("", listProducts)
productRoutes.GET("/:id", getProduct)
}
Grouping dengan Middleware Khusus
Salah satu keunggulan besar route grouping adalah kemampuan menerapkan middleware hanya pada sekelompok route tertentu, tanpa memengaruhi route lain.
go
v1 := router.Group("/api/v1")
// Route publik, tidak perlu autentikasi
authRoutes := v1.Group("/auth")
{
authRoutes.POST("/login", login)
authRoutes.POST("/register", register)
}
// Route yang butuh autentikasi
protectedRoutes := v1.Group("/")
protectedRoutes.Use(middleware.AuthRequired())
{
protectedRoutes.GET("/profile", getProfile)
protectedRoutes.PUT("/profile", updateProfile)
}
// Route khusus admin, butuh autentikasi + role check
adminRoutes := v1.Group("/admin")
adminRoutes.Use(middleware.AuthRequired(), middleware.RequireRole("admin"))
{
adminRoutes.GET("/dashboard", getDashboard)
adminRoutes.DELETE("/users/:id", deleteUserByAdmin)
}
Dengan pendekatan ini, terlihat jelas mana route yang publik, mana yang butuh login, dan mana yang khusus admin — hanya dengan melihat struktur grouping-nya.
Memisahkan Grouping ke File Terpisah (Struktur yang Rapi)
Untuk project berskala menengah ke atas, sebaiknya route tidak didefinisikan langsung di main.go, melainkan dipisah per modul, misalnya:
go
// internal/modules/user/delivery/http/routes.go
func RegisterUserRoutes(router *gin.RouterGroup, handler *UserHandler) {
users := router.Group("/users")
{
users.GET("", handler.List)
users.GET("/:id", handler.GetByID)
users.POST("", handler.Create)
users.PUT("/:id", handler.Update)
users.DELETE("/:id", handler.Delete)
}
}
go
// cmd/api/main.go
v1 := router.Group("/api/v1")
userHandler := http.NewUserHandler(userUsecase)
http.RegisterUserRoutes(v1, userHandler)
Pola ini membuat setiap modul bertanggung jawab atas route-nya sendiri, sehingga main.go tetap bersih dan mudah dibaca meskipun jumlah modul terus bertambah.
Kesalahan Umum yang Perlu Dihindari
- Menaruh semua route di satu file besar — sulit di-maintain saat endpoint sudah puluhan atau ratusan.
- Tidak memvalidasi path parameter — misalnya langsung memakai
c.Param("id")tanpa cek apakah formatnya valid sebagai angka atau UUID. - Query parameter tanpa default value — menyebabkan bug saat client lupa mengirim parameter seperti
pageataulimit. - Menerapkan middleware secara manual di tiap route — alih-alih memanfaatkan
Group().Use(), sehingga rawan lupa menambahkan middleware di salah satu endpoint. - Konflik path parameter — misalnya mendaftarkan
/users/:iddan/users/searchdi level yang sama bisa menimbulkan ambiguitas; sebaiknya route statis didaftarkan sebelum route dinamis atau diberi prefix yang jelas.
Penutup
Routing yang rapi bukan hanya soal endpoint yang berfungsi, tapi soal seberapa mudah tim (termasuk diri sendiri di masa depan) memahami struktur API hanya dengan melihat definisi route-nya. Dengan menguasai path parameter untuk data dinamis, query parameter untuk filter dan pagination, serta route grouping untuk mengorganisir endpoint dan middleware, aplikasi Gin akan jauh lebih mudah dikembangkan seiring bertambahnya fitur dan skala project.