Kuasai Routing di Gin: Rahasia Path Parameter, Query, dan Grouping yang Rapi

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 parameterquery 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: GETPOSTPUTPATCHDELETEOPTIONSHEAD, 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

  1. Menaruh semua route di satu file besar — sulit di-maintain saat endpoint sudah puluhan atau ratusan.
  2. Tidak memvalidasi path parameter — misalnya langsung memakai c.Param("id") tanpa cek apakah formatnya valid sebagai angka atau UUID.
  3. Query parameter tanpa default value — menyebabkan bug saat client lupa mengirim parameter seperti page atau limit.
  4. Menerapkan middleware secara manual di tiap route — alih-alih memanfaatkan Group().Use(), sehingga rawan lupa menambahkan middleware di salah satu endpoint.
  5. Konflik path parameter — misalnya mendaftarkan /users/:id dan /users/search di 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.

News