This episode covers Gin's routing patterns: path parameters with :id, wildcards with *filepath, query parameters, HTTP method matching, and RouterGroups for v1/v2 prefixes plus 404/405 handling.

Routes are the heart of a REST API: you map a combination of method and path to a handler. Gin offers expressive and efficient routing patterns — path parameters, wildcards, query parameters, and grouping routes with prefixes. Episode 4 systematically dissects all these patterns, including how Gin handles unmatched routes (404) and wrong methods (405).
Path parameters allow dynamic values in URLs. Define them with the colon prefix :name:
r := gin.Default()
r.GET("/users/:id", func(c *gin.Context) {
id := c.Param("id")
c.JSON(200, gin.H{"id": id})
})curl http://localhost:8080/users/42The output is {"id":"42"} — the value 42 is captured by :id and retrieved with c.Param("id"). Note that the value is always a string; converting to an integer is done manually with strconv.
A wildcard (asterisk) matches the rest of the path, including slashes. This pattern is ideal for serving files:
r.GET("/files/*filepath", func(c *gin.Context) {
path := c.Param("filepath")
c.String(200, "path: %s", path)
})curl http://localhost:8080/files/css/style.css
curl http://localhost:8080/files/js/app.jsThe key differences between :id and *filepath:
:id matches only a single path segment (no slashes).*filepath matches the entire remaining path (/css/style.css).Extra data in a URL can be carried as query parameters after the question mark:
r.GET("/users", func(c *gin.Context) {
page := c.DefaultQuery("page", "1")
limit := c.Query("limit")
c.JSON(200, gin.H{
"page": page,
"limit": limit,
})
})curl "http://localhost:8080/users?page=2&limit=10"c.DefaultQuery("page", "1") returns a default value when the parameter is absent, while c.Query("limit") returns an empty string when it's missing. Gin also has c.QueryArray for repeated parameters like tag=a&tag=b.
Gin separates handlers by method, so the same path can serve many operations:
r.GET("/items", listItems)
r.POST("/items", createItem)
r.PUT("/items/:id", updateItem)
r.DELETE("/items/:id", deleteItem)GET is idempotent and doesn't change data, POST creates a new resource, PUT updates the whole resource, and DELETE removes it. This is the standard REST CRUD pattern.
There's a limitation to remember: within one group, paths with the same method must not be ambiguous. For example:
r.GET("/users/new", h1)
r.GET("/users/:id", h2)The code above will panic because Gin can't decide which matches /users/new — the literal new or the :id parameter. The solution: avoid mixing a literal with a parameter in the same segment position, or use an unambiguous pattern.
As your API grows, you'll want to group routes with a shared prefix. RouterGroup answers that need:
r := gin.Default()
v1 := r.Group("/api/v1")
v2 := r.Group("/api/v2")
v1.GET("/users", func(c *gin.Context) {
c.JSON(200, gin.H{"version": "v1", "users": "..."})
})
v2.GET("/users", func(c *gin.Context) {
c.JSON(200, gin.H{"version": "v2", "users": "..."})
})The function r.Group("/api/v1") returns a *RouterGroup. Routes registered inside it automatically get the /api/v1 prefix. This pattern is very common for API versioning.
A RouterGroup can have its own middleware and can be nested:
admin := r.Group("/api/admin")
admin.Use(requireAdmin)
admin.GET("/stats", func(c *gin.Context) {
c.JSON(200, gin.H{"stats": "..."})
})
adminUsers := admin.Group("/users")
adminUsers.GET("", listUsers)
adminUsers.GET("/:id", getUser)The requireAdmin middleware (which we'll write in episodes 6 and 13) only applies to routes inside the admin group. This grouping keeps the code clean as the number of routes grows.
Gin returns 404 for unknown paths and 405 for wrong methods by default. You can customize both:
r := gin.Default()
r.NoRoute(func(c *gin.Context) {
c.JSON(404, gin.H{
"error": "not found",
"message": "route tidak ditemukan",
})
})
r.NoMethod(func(c *gin.Context) {
c.JSON(405, gin.H{"error": "method not allowed"})
})To activate NoMethod, set the HandleMethodNotAllowed flag:
gin.SetMode(gin.ReleaseMode)
r := gin.New()
r.HandleMethodNotAllowed = true
r.NoMethod(func(c *gin.Context) {
c.JSON(405, gin.H{"error": "method not allowed"})
})The functions r.NoRoute(...) and r.NoMethod(...) enable consistent error responses — important for API documentation and developer experience.
Key takeaways:
:id for a single segment, *filepath for the rest of the path.c.Query and c.DefaultQuery.NoRoute and NoMethod.In the next episode, episode 5, we'll dissect request binding & validation — mapping JSON, query, form, URI, XML, YAML, and TOML to structs, understanding the json, form, uri, and binding tags, and the differences between ShouldBind, MustBind, and Bind.