Learn Gin - Routing & Path Parameters
Series/Learn Gin/Episode 4
Episode 4 of 23

Learn Gin - Routing & Path Parameters

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.

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

Introduction

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).

Basic Route Patterns

Path Parameters with a Colon

Path parameters allow dynamic values in URLs. Define them with the colon prefix :name:

Path parameter :id
r := gin.Default()
 
r.GET("/users/:id", func(c *gin.Context) {
    id := c.Param("id")
    c.JSON(200, gin.H{"id": id})
})
Test the path parameter
curl http://localhost:8080/users/42

The 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.

Wildcards with an Asterisk

A wildcard (asterisk) matches the rest of the path, including slashes. This pattern is ideal for serving files:

Wildcard *filepath
r.GET("/files/*filepath", func(c *gin.Context) {
    path := c.Param("filepath")
    c.String(200, "path: %s", path)
})
Test the wildcard
curl http://localhost:8080/files/css/style.css
curl http://localhost:8080/files/js/app.js

The key differences between :id and *filepath:

  • :id matches only a single path segment (no slashes).
  • *filepath matches the entire remaining path (/css/style.css).

Query Parameters

Extra data in a URL can be carried as query parameters after the question mark:

Reading query parameters
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,
    })
})
Test query parameters
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.

HTTP Method Matching

Gin separates handlers by method, so the same path can serve many operations:

One path, many methods
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:

Example of a route conflict
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.

RouterGroup

As your API grows, you'll want to group routes with a shared prefix. RouterGroup answers that need:

RouterGroup v1 and v2
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.

Middleware and Nested Groups

A RouterGroup can have its own middleware and can be nested:

Group with middleware and nesting
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.

Handling 404 and 405

Gin returns 404 for unknown paths and 405 for wrong methods by default. You can customize both:

Custom 404 and 405
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:

Enable 405 handling
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.

Closing

Key takeaways:

  • :id for a single segment, *filepath for the rest of the path.
  • Query parameters are read with c.Query and c.DefaultQuery.
  • One path can serve many methods: GET, POST, PUT, DELETE.
  • Avoid mixing literals and parameters in the same segment.
  • RouterGroup makes versioning and per-area middleware easy.
  • Custom 404/405 with 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.

Learn Gin - Routing & Path Parameters | Learn Gin