Go Routing 101: Handling and Grouping Routes with net/http

Kengo Wada - Nov 2 - - Dev Community

Go 1.22 brings two enhancements to the net/http package’s router: method matching and wildcards. These features let you express common routes as patterns instead of Go code. Although they are simple to explain and use, it was a challenge to come up with the right rules for selecting the winning pattern when several match a request.

Go 1.22 added new features to their net/http package to make it a good alternative to using third-party libraries. In this article, we will look at how to handle routing using Golang's net/http package. We will start with the basic route handling and then move on to grouping those routes.

Notes

  • This assumes you're using Go version >= 1.22
  • The repo for more details

Basic Routing

Let us start by looking at how to register your routes.

// main.go
package main

import (
    "log"
    "net/http"
)

func main() {
    router := http.NewServeMux()

    router.HandleFunc("GET /users/", getUsers)
    router.HandleFunc("POST /users/", createUser)
    router.HandleFunc("GET /users/{id}/", getUser)
    router.HandleFunc("DELETE /users/{id}/", deleteUser)

    err := http.ListenAndServe(":8000", router)
    if err != nil {
        log.Fatal(err)
    }
}

// Here goes the implementation for getUsers, getUser, createUser, deleteUser
// Check the repo in services/users/routes.go

type User struct {
    ID    int    `json:"id"`
    Name  string `json:"name"`
    Email string `json:"email"`
}

var users []User = []User{
    {ID: 1, Name: "Bumblebee", Email: "bumblebee@autobots.com"},
    {ID: 2, Name: "Optimus Prime", Email: "optimus.prime@autobots.com"},
    {ID: 3, Name: "Ironhide", Email: "ironhide@autobots.com"},
    {ID: 4, Name: "Hot Rod", Email: "hot.rod@autobots.com"},
}

func getUsers(w http.ResponseWriter, r *http.Request) {
    response := map[string]any{
        "message": "Done",
        "users":   users,
    }
    utils.WriteJSONResponse(w, http.StatusOK, response)
}
Enter fullscreen mode Exit fullscreen mode

Let's go through the above code:

  1. router := http.NewServeMux() This creates a new request multiplexer. When a request is made, the router examines the request’s URL and selects the most appropriate handler to serve the request.
  2. router.HandleFunc("GET /users/", getUsers) This registers the /users/ route and indicates that this will be a GET method route.
  3. utils.WriteJSONResponse This is a utility function to create a JSON response and can be found in the repo in utils/utils.go

Note: When making requests make sure to add the trailing slash. Doing otherwise will return a 404 not found response
Example:

  • http://localhost:8000/users returns 404
  • http://localhost:8000/users/ returns the correct server response

Sample Request:

  • Request: GET http://localhost:8000/users/
  • Response:
// statusCode: 200
{
    "message": "Done",
    "users": [
        {
            "id": 1,
            "name": "Bumblebee",
            "email": "bumblebee@autobots.com"
        },
        {
            "id": 2,
            "name": "Optimus Prime",
            "email": "optimus.prime@autobots.com"
        },
        {
            "id": 3,
            "name": "Ironhide",
            "email": "ironhide@autobots.com"
        },
        {
            "id": 4,
            "name": "Hot Rod",
            "email": "hot.rod@autobots.com"
        }
    ]
}
Enter fullscreen mode Exit fullscreen mode

Grouping Routes

As we can see from the above, this would require us to register all endpoints in the same place and can get out of hand quickly. Grouping routes helps you keep your code organized, scalable, and maintainable by putting related routes and logic together. It allows you to apply middleware selectively, encourages reusability, and improves readability, especially as your application grows.
Now let us look at how we can group routes

We will start by registering routes locally in the package where their handler functions are defined. The next step is to bring all those various routes together and start the server.

// services/users/routes.go
package user

import (
    "fmt"
    "net/http"
    "strconv"

    "<your-project-name>/gorouting/utils"
)

type Handler struct{}

func NewHandler() *Handler {
    return &Handler{}
}

func (h *Handler) RegisterRoutes() *http.ServeMux {
    r := http.NewServeMux()
    r.HandleFunc("GET /", getUsers)
    r.HandleFunc("POST /", createUser)

    r.HandleFunc("GET /{id}/", getUser)
    r.HandleFunc("DELETE /{id}/", deleteUser)

    return r
}
// ...
Enter fullscreen mode Exit fullscreen mode

Let's go through the code.

  1. func NewHandler() *Handler This creates a new Handler that is used for dependency injection such as adding access to a database should one be present.
  2. func (h *Handler) RegisterRoutes() *http.ServeMux Here we create a new ServeMux and register routes.
// cmd/api/api.go
package api

import (
    "log"
    "net/http"

    "<your-project-name>/services/user"
)

type APIServer struct {
    addr string
}

func NewAPIServer(addr string) *APIServer {
    return &APIServer{addr: addr}
}

func (s *APIServer) Run() error {
    userHandler := user.NewHandler()
    userRouter := userHandler.RegisterRoutes()

    router := http.NewServeMux()
    router.Handle("/users/", http.StripPrefix("/users", userRouter))

    log.Println("Starting server on port", s.addr)
    return http.ListenAndServe(s.addr, router)
}
Enter fullscreen mode Exit fullscreen mode

Here we are going to focus on the Run method.

  1. userHandler := user.NewHandler() This creates a new handler and this is the point where things like database connections can be passed along to the endpoints that need them. This is called dependency injection.
  2. userRouter := userHandler.RegisterRoutes() This creates a ServeMux containing the user routes.
  3. router.Handle("/api/v1/users/", http.StripPrefix("/api/v1/users", userRouter)) This registers the users with the base URL of /users/. StripPrefix removes the specified prefix from the request URL before routing it to userRouter.
// cmd/main.go
package main

import (
    "log"

    "<your-project-name>/cmd/api"
)

func main() {
    server := api.NewAPIServer(":8000")
    if err := server.Run(); err != nil {
        log.Fatal(err)
    }
}
Enter fullscreen mode Exit fullscreen mode

“With Go 1.22, net/http is now more versatile, offering route patterns that improve clarity and efficiency. This approach to grouping routes shows how easy it is to maintain scalable code while taking advantage of Go’s built-in routing capabilities.” ChatGPT

Now that we have managed to group the user routes. Clone the repo and try adding another service.

. .