9 minutes read

One of the features of a web server is its routing capabilities. Each endpoint must have a specific handler. But what if there are really a lot of routes? For instance, a news website can form a route by category /news/technology/IT/Golang. Or the URLs of the articles can be represented as follows /article/the-name-of-the-article. In this case, creating a separate route for each category or article would be redundant. After all, in the first case, a category is just a list filtering method, and in the second, one handler is enough for all articles; you just need to teach it to read the article's title from the link.

In this topic, you'll learn what features the Gin framework provides for working with routes.

Basic routing

You might remember that a route is defined by three main parameters: a network method, a name, and a handler. Let's focus on the endpoint name and solve the following task: you must create a web server with the /api endpoint. The /api endpoint should have three routes: /login, /logout, and /user. Each route should return a string as a response:

  • the /api/login route returns the string "login";

  • the /api/logout route returns the string "logout";

  • the /api/user route returns the string "user".

Of course, you can just create routes that are independent of each other. For example:

package main

import (
    "github.com/gin-gonic/gin"
    "log"
    "net/http"
)

func main() {
    router := gin.Default()
    router.GET("/api/login", func(context *gin.Context) {
        context.String(http.StatusOK, "login")
    })
    router.GET("/api/logout", func(context *gin.Context) {
        context.String(http.StatusOK, "logout")
    })
    router.GET("/api/user", func(context *gin.Context) {
        context.String(http.StatusOK, "user")
    })

    err := router.Run()
    if err != nil {
        log.Fatal(err)
    }
}

The above code is a simple solution for creating the three routes within the /api endpoint. Gin's router.GET method provides a simple way to create a route with handler functions that return specific string responses for each route.

Grouping routes

Now imagine a scenario where you're working with a web application that contains more than 20 routes, and you need to rename the root endpoint of /api to /api_v2 or specify an additional internal route name such as /api/v2 (and keep all the other routes).

You can solve this task in a different way using another feature of the Gin framework. Let's allocate the route /api to an independent group using the router.Group method, and create endpoints specific to that group:

package main

import (
    "github.com/gin-gonic/gin"
    "log"
    "net/http"
)

func main() {
    router := gin.Default()

    apiGroup := router.Group("/api")

    apiGroup.GET("/login", func(context *gin.Context) {
        context.String(http.StatusOK, "login")
    })
    apiGroup.GET("/logout", func(context *gin.Context) {
        context.String(http.StatusOK, "logout")
    })
    apiGroup.GET("/user", func(context *gin.Context) {
        context.String(http.StatusOK, "user")
    })

    err := router.Run()
    if err != nil {
        log.Fatal(err)
    }
}

A group can be considered a subset of a router that allows you to organize routes into smaller collections with similar capabilities for defining routes and handling requests. However, note that in contrast to a router, a group cannot function as a standalone web server.

A small example below showcases how to create a nested route with a depth of three levels:

package main

import (
    "net/http"
    "github.com/gin-gonic/gin"
)

func main() {
    router := gin.Default()

    level1 := router.Group("/level1")
    level2 := level1.Group("/level2")
    level3 := level2.Group("/level3")

    level3.GET("/ping", func(context *gin.Context) {
        context.String(http.StatusOK, "pong")
    })

    err := router.Run()
    if err != nil {
        log.Fatal(err)
    }
}

A request to localhost:8080/level1/level2/level3/ping should return the string "pong".

Group handlers

Moreover, Gin allows you to define a group handler that performs a common task for all endpoints within a group. However, its behavior is different from the individual endpoint handler. In the example below, two handlers are defined for the /api route. The first is defined in the usual way using the router.GET method, but the second is defined using the router.Group method.

package main

import (
    "github.com/gin-gonic/gin"
    "log"
    "net/http"
)

func main() {
    router := gin.Default()

    router.GET("/api", func(context *gin.Context) {
        context.String(http.StatusOK, "Individual /api handler")
    })

    apiGroup := router.Group("/api", func(context *gin.Context) {
        context.String(http.StatusOK, "Group /api handler ")
    })

    apiGroup.GET("/login", func(context *gin.Context) {
        context.String(http.StatusOK, "login")
    })
    apiGroup.GET("/logout", func(context *gin.Context) {
        context.String(http.StatusOK, "logout")
    })

    err := router.Run()
    if err != nil {
        log.Fatal(err)
    }
}

The group handler is triggered every time a request is made to any endpoint within the group, but the endpoint must be specified using the individual handler function for that endpoint.

If you make a request to the /api route, you should see the "Individual /api handler" string (nothing unusual). However, if you make a request to the /api/login route, the response will be "Group /api handler login".

This happens because Gin combines the response generated by the group handler with the response generated by the endpoint handler for the specific endpoint (in this case, /login). This feature can be useful if you want to restrict access to a group of endpoints, or create a route-based filter.

Route parameters

Let's recall from the introduction a possible way to represent URLs of articles in a website: /article/the-name-of-the-article. In this scenario, router groups cannot help you determine the title of each article; however, Gin provides route parameters that can be used to create dynamic and flexible endpoints that can handle many requests.

Now, suppose you wanted to create a web server with the /article endpoint. The second level of the route should be the article title, for example /article/golang-gin, and when a request is made to the article route, your web server should respond with a string including the article title such as "The article title is golang-gin".

To define a router parameter in Gin, use the colon symbol : followed by the parameter name. For example, /article/:title where title is the name of the route parameter.

package main

import (
    "github.com/gin-gonic/gin"
    "log"
    "net/http"
)

type Article struct {
    Title string `uri:"title"`
}

func main() {
    router := gin.Default()

    router.GET("/article/:title", func(context *gin.Context) {
        var article Article

        err := context.ShouldBindUri(&article)
        if err != nil {
            context.String(http.StatusBadRequest, "error to specify Title")
            return
        }

        context.String(http.StatusOK, "The article title is %s", article.Title)
    })

    err := router.Run()
    if err != nil {
        log.Fatal(err)
    }
}

If you run the code of the above example and make a request to the /article/basic-routing endpoint, it should return the "The article title is basic-routing" string. To define the parameters, you need to use the ShouldBindUri method of the request context; it takes a pointer to the Article struct as an argument, which afterward is used to bind the route parameters to the URL.

Note that the struct passed as an argument must contain the route parameter(s) as field(s), with each field having a struct tag with the uri key to specify the corresponding route parameter name. For example, in the above code snippet, the Article struct, has the Title field followed by the struct tag `uri:"title"`.

Conclusion

Today you have become acquainted with Gin's capabilities to declare routes. Let's recap the main points:

  • The routes can be grouped using the Group method;

  • The group can be defined with a handler;

  • The group can include other groups;

  • The route can include parameters defined by the colon (:) symbol;

  • Route parameters are obtained using the ShouldBindUri method into a declared struct.

2 learners liked this piece of theory. 0 didn't like it. What about you?
Report a typo