Routing Requests with Mux

Coal
5 min readMay 23, 2023

--

Now that we’ve seen a bit about convention over configuration, let’s start writing a Go router that is more similar to the router existing in Rails.

To do this, we will make use of Gorilla Mux, which is a powerful HTTP router for the Go programming language. It is built on top of Go’s standard “net/http” package but provides an additional layer of functionalities and features.

Mux is a flexible and efficient routing library that allows you to easily create APIs and web applications. It provides a set of features that simplify route management, middleware handling, and HTTP request and response manipulation.

Some of the key features of Gorilla Mux include:

  1. Powerful routing: Mux allows you to define routes based on URL patterns, route parameters, and regular expressions. This makes it easy to define how your application should behave for different HTTP requests.
  2. Parameter handling: Mux enables you to extract parameters from the URL, such as resource IDs or query values, and pass them to route handlers. This makes it easy to build flexible APIs that can handle different types of requests.
  3. Middleware: Mux supports adding middleware to route handlers. This allows you to execute functions before or after processing a route, adding features such as authentication, request logging, response compression, and more.
  4. Error handling: Mux provides a convenient way to handle errors and return custom responses in case of issues during request processing.
  5. Compatibility with the “net/http” package: Mux is built on top of Go’s standard “net/http” package, which means you can combine Mux with other features and functionalities from the Go HTTP ecosystem.

Using Gorilla Mux, you can build a robust and flexible router in Go that resembles the router in Rails.

When implementing a service using Gorilla Mux, it is common to create the handlers first and then configure the routes to be handled by the appropriate handlers.

package main

import (
"fmt"
"log"
"net/http"

"github.com/gorilla/mux"
)

func HomeHandler(w http.ResponseWriter, r *http.Request) {
fmt.Fprintln(w, "Home page")
}

func AboutHandler(w http.ResponseWriter, r *http.Request) {
fmt.Fprintln(w, "About Page")
}

func main() {
router := mux.NewRouter()
router.HandleFunc("/", HomeHandler)
router.HandleFunc("/about", AboutHandler)
log.Fatal(http.ListenAndServe(":8080", router))
}

This is an excellent pattern for microservices. It is concise, easily understandable, and highly testable. However, our goal here is to build something that caters to a monolithic application, and for that, we will do something very similar to what Rails does, while respecting its limitations.

To start, I want my router to follow a naming convention that is easy to understand, as succinctly summarized in Carly L.’s “Ruby on Rails: Naming Convention Cheatsheet.

When configuring a route in Rails, we typically do something like this:

get '/patients/:id', to: 'patients#show'

When we do this, Rails automatically understands that any content arriving at the route “patients/:id” should be directed to the “show” method of the “Patients” controller.

Let’s get to work!

The first thing we want for our router is the dynamic registration of its handlers. This will allow us to add routes in a named manner so that the router can resolve and direct them to the correct handler.

╔═══════════╦══════════════════════╦═══════════════════╗═════════════════════════════════════════════════╗
║ HTTP Verb ║ Path ║ Controller#Action ║ Used for ║
╠═══════════╬══════════════════════╬═══════════════════╬═════════════════════════════════════════════════╣
║ GET ║ /resources ║ resources#index ║ display a list of all resources ║
║ GET ║ /resources/new ║ resources#new ║ return an HTML form for creating a new resource ║
║ POST ║ /resources ║ resources#create ║ create a new photo resource ║
║ GET ║ /resources/:id ║ resources#show ║ display a specific resource ║
║ GET ║ /resources/:id ║ resources#update ║ return an HTML form for editing a resource ║
║ PATCH/PUT ║ /resources/:id/edit ║ resources#edit ║ update a specific resource ║
║ DELETE ║ /resources/:id ║ resources#destroy ║ delete a specific resource ║
╚═══════════╩══════════════════════╩═══════════════════╩═════════════════════════════════════════════════╝

Basically, what we’re going to do is create a map of strings where the value corresponds to the handler responsible for processing the route. The handler in Mux takes two arguments: http.ResponseWriter and *http.Request. These two arguments provide information about the received HTTP request and allow you to send a response back to the client.

The http.ResponseWriter is an interface that allows you to write the HTTP response back to the client. You can use it to set the response status, headers, and body.

The *http.Request is a pointer to a structure that represents the received HTTP request. It contains information about the HTTP method (GET, POST, etc.), the request headers, URL parameters, request body data, and other relevant information.

// example/router/router.go
package router

import "net/http"

type Registry map[string]func(w http.ResponseWriter, r *http.Request)

Let’s also add a function to help us extract some metadata based on the index of each handler.

// example/router/solv/solv.go
package solv

import "strings"

func Route( name string ) (string, bool) {
var httpVerb string;
var hasId = false
split := strings.Split(name, "#")
switch split[1] {
case "index", "new", "show", "edit":
if split[1] == "show" || split[1] == "edit" { hasId = true }
httpVerb = "GET"
case "create":
httpVerb = "POST"
case "update":
hasId = true
httpVerb = "PUT"
case "destroy":
hasId = true
httpVerb = "DELETE"
default:
httpVerb = ""
}
return httpVerb, hasId
}

As we can see, it checks the actions to determine which method triggers the action. Additionally, it also returns a condition indicating whether the route requires the “id” parameter in its URL and the name of the controller responsible for the route.

Let’s also create a type for our router and add a function to start it.

// example/router/router.go
package router

import (
"net/http"
"github.com/gorilla/mux"
)

type Registry map[string]func(w http.ResponseWriter, r *http.Request)

type Router struct {
provider *mux.Router
}

func ( this *Router ) Start() {
log.Println("Server started on port 8080")
log.Fatal(http.ListenAndServe(":8080", this.provider))
}

Now we can check if our router is starting correctly.

// example/main.go

package main


import (
"example/router"
)

func main() {
r := router.Router{}
r.Start()
}
> go run main.go
2023/05/23 13:49:26 Server started on port 8080

Registering the Routes

Now that our router is working correctly, let’s start adding the logic responsible for registering the handlers for each route.

First, let’s modify our type to have an additional field responsible for storing our array of handlers. We’ll also add the Push method to insert new handlers into the map.

// example/router/router.go
package router

import (
"net/http"
"github.com/gorilla/mux"
"log"
)

type Registry map[string]func(w http.ResponseWriter, r *http.Request)

type Router struct {
provider *mux.Router
registry Registry
}

func ( this *Router ) Start() {
log.Println("Server started on port 8080")
log.Fatal(http.ListenAndServe(":8080", this.provider))
}

func ( this *Router ) Push(name string, handler func(w http.ResponseWriter, r *http.Request)) {
if this.registry == nil{ this.registry = make(Registry) }
this.registry[name] = handler
}
}

And adjust our main function to add the first route.

// example/main.go
package main

import (
"example/router"
"net/http"
"fmt"
)

func Hello(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusOK)
w.Header().Set("Content-Type", "text/plain")
fmt.Fprintln(w, "hello you")
}

func main() {
r := router.Router{}
r.Push("hello#index", Hello)
r.Start()
}

And finally, let’s adjust our start function to register the routes before starting. The final code for our router looks like this:

// example/router/router.go
package router

import (
"net/http"
"github.com/gorilla/mux"
"log"
"example/router/solv"
"path"
)

type Registry map[string]func(w http.ResponseWriter, r *http.Request)

type Router struct {
provider *mux.Router
registry Registry
}

func ( this *Router ) Start() {
this.provider = mux.NewRouter()
for name, handler := range this.registry {
verb, id, controller := solv.Route(name)

url := path.Join("/", controller);
if id { url = path.Join(url, ":id")}
this.provider.HandleFunc(url, handler).Methods(verb)
}
log.Println("Server started on port 8080")
log.Fatal(http.ListenAndServe(":8080", this.provider))
}

func ( this *Router ) Push(name string, handler func(w http.ResponseWriter, r *http.Request)) {
if this.registry == nil{ this.registry = make(Registry) }
this.registry[name] = handler
}
// example/main.go
package main

import (
"example/router"
"net/http"
"fmt"
)

func Hello(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusOK)
w.Header().Set("Content-Type", "text/plain")
fmt.Fprintln(w, "hello you")
}

func main() {
r := router.Router{}
r.Push("hello#index", Hello)
r.Start()
}

In the next article, we will continue the implementation to include configurable semantics similar to what we find in Rails.

--

--

No responses yet