Handling Routing for our REST Services
In this chapter, we will discuss routing for a REST application. To create an API, the first step is to define routes. To define routes, we have to figure out the available system packages in Go. We'll begin this chapter by exploring the basic internal routing mechanism in Go. We then see how to create a custom multiplexer, an entity that matches a given URL to a registered pattern. A multiplexer basically allows a developer to create a route to listen to client requests and attaches handlers that hold the business logic of the application. The ServeMux package is the basic multiplexer provided by Go. We'll then explore a few other frameworks as ServeMux capabilities are very limited.
This chapter also includes the likes of third-party libraries such as httprouter and gorilla/mux. Then, we'll discuss topics such as SQL injection. The crux of this chapter is to teach you how to create elegant HTTP routers in Go using gorilla/mux. We'll also briefly discuss designing a URL shortening service.
We will cover the following topics:
- Understanding Go's net/http package
- ServeMux—a basic router in Go
- Understanding httprouter—a lightweight HTTP router
- Introducing gorilla/mux—a powerful HTTP router
- Reader's challenge: an API for URL shortening
Technical requirements
The following are the software that should be pre-installed for running code samples:
- OS: Linux (Ubuntu 18.04)/Windows 10/Mac OS X >=10.13
- Go latest version compiler >= 1.13.5
You can download the code for this chapter from https://github.com/PacktPublishing/Hands-On-Restful-Web-services-with-Go/tree/master/chapter2. Clone the code and use the code samples in the chapter2 directory.
Understanding Go's net/http package
Accepting HTTP requests is the primary goal of a web server. In Go, there is a system-level package that helps developers create HTTP servers and clients. The name of the package is net/http. We can understand the functionality of the net/http package by creating a small example. The example accepts an incoming request and returns the timestamp of the server. Let us see the steps for creating such a server:
- Create the program file as follows:
touch -p $GOPATH/src/github.com/git-user/chapter2/healthCheck/main.go
Now, we have a file where we can develop a server with a Health Check API that returns a date/time string.
- Import the net/http package and create a function handler called HealthCheck. The http.HandleFunc is a method that takes a route and a function handler as its arguments. This function handler has to return an http.ResponseWriter object:
package main
import (
"io"
"log"
"net/http"
"time"
)
// HealthCheck API returns date time to client
func HealthCheck(w http.ResponseWriter, req *http.Request) {
currentTime := time.Now()
io.WriteString(w, currentTime.String())
}
func main() {
http.HandleFunc("/health", HealthCheck)
log.Fatal(http.ListenAndServe(":8000", nil))
}
The preceding code creates a HealthCheck function and attaches it to an HTTP route. HandleFunc is used to attach a route pattern to a handler function. ListenAndServe starts a new HTTP server. It returns an error if the server launch is unsuccessful. It takes address:port as the first argument and the second argument is nil, which says use the default multiplexer. We will see multiplexers in detail in the upcoming sections.
- Now, we can start the web server using this command:
go run $GOPATH/src/github.com/git-user/chapter2/healthCheck/main.go
Run the healthCheck.go file from a shell.
- Now, fire up a shell or browser to see the server in action. Here, we use the curl request:
curl -X GET http://localhost:8000/health
The response is as follows:
2019-04-10 17:54:05.450783 +0200 CEST m=+6.612810181
Go has a different concept for handling request and response. We used the io library to write to the response. For web development, we can use a template to automatically fill in the details. Go's internal URL handlers use a ServeMux multiplexer. In the next section, we will discuss more on ServeMux, a built-in URL router in Go.
ServeMux – a basic router in Go
ServeMux is an HTTP request multiplexer. The HandleFunc we used in the preceding section is actually a method of ServeMux. By using ServeMux, we can handle multiple routes. We can also create our own multiplexer. A multiplexer handles the logic of separating routes with a function called ServeHTTP. So, if we create a Go struct with the ServeHTTP method, it can do the job as the in-built multiplexer.
Consider a route as a key in a Go dictionary (map) and a multiplexer as its value. Go finds the multiplexer from the route and tries to execute the ServeHTTP function. In the following section, we will see the usage of ServeMux by creating an API that generates UUID strings.
Developing a UUID generation API using ServeMux
A UUID is a unique identifier for a resource or a transaction. UUIDs are widely used for identifying an HTTP request. Let us develop an API for generating a UUID. Please follow these steps:
- Create the program file as follows:
touch -p $GOPATH/src/github.com/git-user/chapter2/uuidGenerator/main.go
- Any Go struct with a few dedicated HTTP methods is qualified to be a ServeMux. For example, we can create a custom UUID struct and implement the ServeHTTP function in order to make it a ServeMux object. Following is the implementation for the uuidGenerator.go module:
import (
"crypto/rand"
"fmt"
)
// UUID is a custom multiplexer
type UUID struct {
}
func (p *UUID) ServeHTTP(w http.ResponseWriter, r *http.Request) {
if r.URL.Path == "/" {
giveRandomUUID(w, r)
return
}
http.NotFound(w, r)
return
}
func giveRandomUUID(w http.ResponseWriter, r *http.Request) {
c := 10
b := make([]byte, c)
_, err := rand.Read(b)
if err != nil {
panic(err)
}
fmt.Fprintf(w, fmt.Sprintf("%x", b))
}
It consists of the UUID struct that acts as a ServeMux object. We can access the URL path in the handler function and use that information to manually route the requests to different response generators.
giveRandomUUID is a response generator function that sets a random UUID string to response. Go's crypto package has a Read function that fills random characters into a byte array.
- Now add a main function to the module using the ServeMux object. We should pass our ServeMux to the http.ListenAndServe function to get our content served. We are serving our content on port 8000:
package main
import (
"net/http"
)
func main() {
mux := &UUID{}
http.ListenAndServe(":8000", mux)
}
We use UUID as a multiplexer in the ListenAndServe function, which starts an HTTP server. The server executes the ServeHTTP method that is defined preceding on the mux object.
- Run the following command from your shell/Terminal:
go run $GOPATH/src/github.com/git-user/chapter2/uuidGenerator/main.go
- We can make a curl request like this to make a request to the web server that is listening on port 8000:
curl -X GET http://localhost:8000/
The response that is returned will be a random string:
544f5519592ac25bb2c0
Until now, we have worked with a single handler. Let us see how we can add multiple handlers to route to different function handlers using ServeMux.
Adding multiple handlers using ServeMux
Let us say we have an API requirement that generates random numbers of different types such as int, float, and so on. The custom multiplexer (mux) we developed can be cumbersome when there are multiple endpoints with different functionalities. To add that logic, we need to add multiple if/else conditions to manually check the URL route. To overcome that complex code structure, we can instantiate a new in-built ServeMux object and define many handlers. Let's look at the code with ServeMux:
newMux := http.NewServeMux()
newMux.HandleFunc("/randomFloat", func(w http.ResponseWriter,
r *http.Request) {
fmt.Fprintln(w, rand.Float64())
})
newMux.HandleFunc("/randomInt", func(w http.ResponseWriter,
r *http.Request) {
fmt.Fprintln(w, rand.Int(100))
})
This code snippet shows how to create a ServeMux and attach multiple handlers to it.
randomFloat and randomInt are the two routes we create for returning a random float and random int, respectively. Now, we pass that to the ListenAndServe function. Int(100) returns a random integer number from the range 0-100.
Let us see a complete example:
- Create a file to hold our program and call it multipleHandlers.go in the following path:
touch -p $GOPATH/src/github.com/git-user/chapter2/multipleHandlers/main.go
- Now create a main function and add the code for creating the ServeMux object and function handlers.
- Finally, run the server with the http.ListenAndServe method:
package main
import (
"fmt"
"math/rand"
"net/http"
)
func main() {
newMux := http.NewServeMux()
newMux.HandleFunc("/randomFloat", func(w http.ResponseWriter,
r *http.Request) {
fmt.Fprintln(w, rand.Float64())
})
newMux.HandleFunc("/randomInt", func(w http.ResponseWriter,
r *http.Request) {
fmt.Fprintln(w, rand.Intn(100))
})
http.ListenAndServe(":8000", newMux)
}
- We can run the program directly using the run command:
go run $GOPATH/src/github.com/git-user/chapter2/multipleHandlers/main.go
- Now, let us fire two curl commands and see the output:
curl -X GET http://localhost:8000/randomFloat
curl -X GET http://localhost:8000/randomInt
The responses will be:
0.6046602879796196
87
We saw how we can create a URL router with basic Go constructs. Let us have a look at a few popular URL routing frameworks that are widely used by the Go community for their API servers.
Understanding httprouter – a lightweight HTTP router
httprouter, as the name suggests, routes the HTTP requests to particular handlers. httprouter is a well-known package in Go for creating simple routers with an elegant API. The developers coming from the Python/Django community are very familiar with a full-blown URL dispatcher in the Django framework. httprouter provides similar features:
- Allows variables in the route paths
- Matches the REST methods (GET, POST, PUT, and so on)
- No compromise of performance
We are going to discuss these qualities in more detail in the following section. Before that, there are a few noteworthy points that make httprouter an even better URL router:
- httprouter plays well with the in-built http.Handler
- httprouter explicitly says that a request can only match to one route or no route
- The router's design encourages building sensible, hierarchical RESTful APIs
- You can build simple and efficient static file servers
In the next section, we see the installation of httprouter and its basic usage.
Installing httprouter
httprouter is an open source Go package and can be installed using the go get command. Let us see the installation and basic usage in the steps given as follows:
- Install httprouter using this command:
go get github.com/julienschmidt/httprouter
We can import the library in our source code, like this:
import "github.com/julienschmidt/httprouter"
- The basic usage of httprouter can be understood through an example.
Let us write a REST service in Go that provides two things:
- Gets the Go compiler version
- Gets the content of a given file
We need to use a system package called os/exec to fetch the preceding details.
- The os/exec package has a Command function, using which we can make any system call and the function signature is this:
// arguments... means an array of strings unpacked as arguments
// in Go
cmd := exec.Command(command, arguments...)
- The exec.Command function takes the command and an additional argument's array. Additional arguments are the options or input for the command. It can then be executed by calling the Output function, like this:
out, err := cmd.Output()
- This program uses httprouter to create the service. Let us create it at the following path:
touch -p $GOPATH/src/github.com/git-user/chapter2/httprouterExample/main.go
The program's main function creates two routes and two function handlers. The responsibilities of function handlers are:
- To get the current Go compiler version
- To get the contents of a file
The program is trying to implement a REST service using httprouter. We are defining two routes here:
- /api/v1/go-version
- /api/v1/show-file/:name
package main
import (
"fmt"
"io"
"log"
"net/http"
"os/exec"
"github.com/julienschmidt/httprouter"
)
func main() {
router := httprouter.New()
router.GET("/api/v1/go-version", goVersion)
router.GET("/api/v1/show-file/:name", getFileContent)
log.Fatal(http.ListenAndServe(":8000", router))
}
:name is a path parameter. The basic Go router cannot define these special parameters. By using httprouter, we can match the REST methods. In the main block, we are matching GET requests to their respective routes.
Now we are coming to the implementation of three handler functions:
func getCommandOutput(command string, arguments ...string) string {
out, _ := exec.Command(command, arguments...).Output()
return string(out)
}
func goVersion(w http.ResponseWriter, r *http.Request, params httprouter.Params) {
response := getCommandOutput("/usr/local/go/bin/go", "version")
io.WriteString(w, response)
return
}
func getFileContent(w http.ResponseWriter, r *http.Request, params httprouter.Params) {
fmt.Fprintf(w, getCommandOutput("/bin/cat", params.ByName("name")))
}
exec.Command takes the bash command and respective options as its arguments and returns an object. That object has an Output method that returns the output result of command execution. We are utilizing this utility getCommandOutput function in both goVersion and getFileContent handlers. We use shell command formats such as go --version and cat file_name in handlers.
Now create two new files in the same directory. These files will be served by our file server program. You can create any custom files in this directory for testing:
Latin.txt:
Lorem ipsum dolor sit amet, consectetuer adipiscing elit. Aenean commodo ligula eget dolor. Aenean massa. Cum sociis natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus. Donec quam felis, ultricies nec, pellentesque eu, pretium quis, sem. Nulla consequat massa quis enim. Donec pede justo, fringilla vel, aliquet nec, vulputate eget, arcu.
Greek.txt:
Οἱ δὲ Φοίνιϰες οὗτοι οἱ σὺν Κάδμῳ ἀπιϰόμενοι.. ἐσήγαγον διδασϰάλια ἐς τοὺς ῞Ελληνας ϰαὶ δὴ ϰαὶ γράμματα, οὐϰ ἐόντα πρὶν ῞Ελλησι ὡς ἐμοὶ δοϰέειν, πρῶτα μὲν τοῖσι ϰαὶ ἅπαντες χρέωνται Φοίνιϰες· μετὰ δὲ χρόνου προβαίνοντος ἅμα τῇ ϕωνῇ μετέβαλον ϰαὶ τὸν ϱυϑμὸν τῶν γραμμάτων. Περιοίϰεον δέ σϕεας τὰ πολλὰ τῶν χώρων τοῦτον τὸν χρόνον ῾Ελλήνων ῎Ιωνες· οἳ παραλαβόντες διδαχῇ παρὰ τῶν Φοινίϰων τὰ γράμματα, μεταρρυϑμίσαντές σϕεων ὀλίγα ἐχρέωντο, χρεώμενοι δὲ ἐϕάτισαν, ὥσπερ ϰαὶ τὸ δίϰαιον ἔϕερε ἐσαγαγόντων Φοινίϰων ἐς τὴν ῾Ελλάδα, ϕοινιϰήια ϰεϰλῆσϑαι.
Now run the program with this command. This time, instead of firing a curl command, let us use the browser as our output for GET. Windows users may not have curl as the first-hand application. They can use API testing software such as the Postman client while developing the REST API. Take a look at the following command:
go run $GOPATH/src/github.com/git-user/chapter2/httprouterExample/main.go
The output for the first GET request looks like this:
curl -X GET http://localhost:8000/api/v1/go-version
The result will be this:
go version go1.13.5 darwin/amd64
The second GET request requesting Greek.txt is:
curl -X GET http://localhost:8000/api/v1/show-file/greek.txt
Now, we will see the file output in Greek:
Οἱ δὲ Φοίνιϰες οὗτοι οἱ σὺν Κάδμῳ ἀπιϰόμενοι.. ἐσήγαγον διδασϰάλια ἐς τοὺς ῞Ελληνας ϰαὶ δὴ ϰαὶ γράμματα, οὐϰ ἐόντα πρὶν ῞Ελλησι ὡς ἐμοὶ δοϰέειν, πρῶτα μὲν τοῖσι ϰαὶ ἅπαντες χρέωνται Φοίνιϰες· μετὰ δὲ χρόνου προβαίνοντος ἅμα τῇ ϕωνῇ μετέβαλον ϰαὶ τὸν ϱυϑμὸν τῶν γραμμάτων. Περιοίϰεον δέ σϕεας τὰ πολλὰ τῶν χώρων τοῦτον τὸν χρόνον ῾Ελλήνων ῎Ιωνες· οἳ παραλαβόντες διδαχῇ παρὰ τῶν Φοινίϰων τὰ γράμματα, μεταρρυϑμίσαντές σϕεων ὀλίγα ἐχρέωντο, χρεώμενοι δὲ ἐϕάτισαν, ὥσπερ ϰαὶ τὸ δίϰαιον ἔϕερε ἐσαγαγόντων Φοινίϰων ἐς τὴν ῾Ελλάδα, ϕοινιϰήια ϰεϰλῆσϑαι.
The endpoint /api/v1/show-file/ we defined in the exec example is not so efficient. Using httprouter, we can build advanced and performance-optimized file servers. In the next section, we'll learn how to do that.
Building a simple static file server in minutes
Sometimes, an API can serve files. The other application of httprouter, apart from routing, is building an efficient file server. It means that we can build a content delivery platform of our own. Some clients need static files from the server. Traditionally, we use Apache2 or Nginx for that purpose. If one has to create something similar purely in Go, they can leverage httprouter.
Let us build one. From the Go server, in order to serve the static files, we need to route them through a universal route, like this:
/static/*
The plan is to use http package's Dir method to load the filesystem, and pass filesystem handler it returns to httprouter. We can use the ServeFiles function of the httprouter instance to attach a router to the filesystem handler. It should serve all the files in the given public directory. Usually, static files are kept in the /var/public/www folder on a Linux machine. Create a folder called static in your home directory:
mkdir -p /users/git-user/static
Now, copy the Latin.txt and Greek.txt files, which we created for the previous example, to the preceding static directory. After doing that, let us write the program for the file server using the following steps. You will be amazed at the simplicity of httprouter:
- Create a program at the following path:
touch -p $GOPATH/src/github.com/git-user/chapter2/fileServer/main.go
- Update the code like the following. You have to add a route that links a static file path route to a filesystem handler:
package main
import (
"log"
"net/http"
"github.com/julienschmidt/httprouter"
)
func main() {
router := httprouter.New()
// Mapping to methods is possible with HttpRouter
router.ServeFiles("/static/*filepath",
http.Dir("/Users/git-user/static"))
log.Fatal(http.ListenAndServe(":8000", router))
}
- Now run the server and see the output:
go run $GOPATH/src/github.com/git-user/chapter2/fileServer/main.go
- Open another Terminal and fire this curl request:
http://localhost:8000/static/latin.txt
- Now, the output will be a static file content server from our file server:
Lorem ipsum dolor sit amet, consectetuer adipiscing elit. Aenean commodo ligula eget dolor. Aenean massa. Cum sociis natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus. Donec quam felis, ultricies nec, pellentesque eu, pretium quis, sem. Nulla consequat massa quis enim. Donec pede justo, fringilla vel, aliquet nec, vulputate eget, arcu.
In the next section, we discuss about a widely used HTTP router called gorilla/mux.
Introducing gorilla/mux – a powerful HTTP router
The word Mux stands for the multiplexer. gorilla/mux is a multiplexer designed to multiplex HTTP routes (URLs) to different handlers. Handlers are the functions that can handle the given requests. gorilla/mux is a wonderful package for writing beautiful routes for our API servers.
gorilla/mux provides tons of options to control how routing is done to your web application. It allows a lot of features, such as:
- Path-based matching
- Query-based matching
- Domain-based matching
- Sub-domain-based matching
- Reverse URL generation
Which type of routing to use depends on the types of clients requesting the server. We first see the installation and then a basic example to understand the gorilla/mux package.
Installing gorilla/mux
Follow these steps to install the mux package:
- You need to run this command in the Terminal (Mac OS X and Linux):
go get -u github.com/gorilla/mux
- If you get any errors saying package github.com/gorilla/mux: cannot download, $GOPATH not set. For more details see--go help gopath, set the $GOPATH environment variable using the following command:
export GOPATH=~/go
- As we discussed in Chapter 1, Getting Started with REST API Development, all the packages and programs go into GOPATH. It has three folders: bin, pkg, and src. Now, add GOPATH to the PATH variable to use the installed bin files as system utilities that have no ./executable style. Refer to the following command:
PATH="$GOPATH/bin:$PATH"
- These settings stay until you turn off your machine. So, to make it a permanent change, add the preceding line to your bash/zsh profile file:
vi ~/.profile
(or)
vi ~/.zshrc
We can import gorilla/mux in our programs, like this:
import "github.com/gorilla/mux"
Now, we are ready to go. Assuming gorilla/mux is installed, we can now explore its basics.
Fundamentals of gorilla/mux
The gorilla/mux package primarily helps to create routers, similar to httprouter. The difference between both is the attachment of a handler function to a given URL. If we observe, the gorilla/mux way of attaching a handler is similar to that of basic ServeMux. Unlike httprouter, gorilla/mux wraps all the information of an HTTP request into a single request object.
The three important tools provided in the gorilla/mux API are:
- The mux.NewRouter method
- The *http.Request object
- The *http.ResponseWriter object
The NewRouter method creates a new router object. That object basically maps a route to a function handler. gorilla/mux passes a modified *http.Request and *http.ResponseWriter object to the function handler. These special objects have lots of additional information about headers, path parameters, request body, and query parameters. Let us explain how to define and use different routers in gorilla/mux with two common types:
- Path-based matching
- Query-based matching
Path-based matching
A path parameter in the URL of an HTTP GET request looks like this:
https://example.org/articles/books/123
Since it is passed after the base URL and API endpoint, in this case https://example.org/articles/, they are called path parameters. In the preceding URL, books and 123 are path parameters. Let us see an example of how to create routes that can consume data supplied as path parameters. Follow these steps:
- Create a new file for our program at the following path:
touch -p $GOPATH/src/github.com/git-user/chapter2/muxRouter/main.go
- The idea is to create a new router, mux.NewRouter, and use it as a handler with in-built http.Server. We can attach URL endpoints to handler functions on this router object. The URL endpoints attached can also be regular expressions. The simple program to collect path parameters from a client HTTP request and return back the same looks like this:
package main
import (
"fmt"
"log"
"net/http"
"time"
"github.com/gorilla/mux"
)
func ArticleHandler(w http.ResponseWriter, r *http.Request) {
vars := mux.Vars(r)
w.WriteHeader(http.StatusOK)
fmt.Fprintf(w, "Category is: %v\n", vars["category"])
fmt.Fprintf(w, "ID is: %v\n", vars["id"])
}
func main() {
r := mux.NewRouter()
r.HandleFunc("/articles/{category}/{id:[0-9]+}", ArticleHandler)
srv := &http.Server{
Handler: r,
Addr: "127.0.0.1:8000",
WriteTimeout: 15 * time.Second,
ReadTimeout: 15 * time.Second,
}
log.Fatal(srv.ListenAndServe())
}
- Now run the server using the following command in a shell:
go run $GOPATH/src/github.com/git-user/chapter2/muxRouter/main.go
- Make a curl request from another shell and we can get the output as follows:
curl http://localhost:8000/articles/books/123
Category is: books ID is: 123
This example shows how to match and parse path parameters. There is one more popular way to collect variable information from an HTTP request and that is with query parameters. In the next section, we see how to create routes that match HTTP requests with query parameters.
Query-based matching
Query parameters are variables that get passed along with the URL in an HTTP request. This is what we commonly see in a REST GET request. The gorilla/mux route can match and collect query parameters. See this following URL, for example:
http://localhost:8000/articles?id=123&category=books
It has id and category as query parameters. All query parameters begin after the ? character.
Let us modify our copy of our previous example into a new one with the name queryParameters/main.go. Modify the route object to point to a new handler called QueryHandler, like this:
// Add this in your main program
r := mux.NewRouter()
r.HandleFunc("/articles", QueryHandler)
In QueryHandler, we can use request.URL.Query() to obtain query parameters from the HTTP request. QueryHandler looks like this:
// QueryHandler handles the given query parameters
func QueryHandler(w http.ResponseWriter, r *http.Request) {
queryParams := r.URL.Query()
w.WriteHeader(http.StatusOK)
fmt.Fprintf(w, "Got parameter id:%s!\n", queryParams["id"][0])
fmt.Fprintf(w, "Got parameter category:%s!", queryParams["category"][0])
}
This program is similar to the previous example, but processes query parameters instead of path parameters.
Run the new program:
go run $GOPATH/src/github.com/git-user/chapter2/queryParameters/main.go
Fire a curl request in this format in a Terminal:
curl -X GET http://localhost:8000/articles\?id\=1345\&category\=birds
We need to escape special characters in the shell. If it is in the browser, there is no problem of escaping. The output looks like this:
Got parameter id:1345!
Got parameter category:birds!
The r.URL.Query() function returns a map with all the parameter and value pairs. They are basically strings and, in order to use them in our program logic, we need to convert the number strings to integers. We can use Go's strconv package to convert a string to an integer, and vice versa.
Other notable features of gorilla/mux
We have seen two basic examples. What next? The gorilla/mux package provides many handy features that makes an API developer's life easy. It gives a lot of flexibility while creating routes. In this section, we try to discuss a few important features. The first feature of interest is generating a dynamic URL with the reverse mapping technique.
In simple words, reverse mapping a URL is getting the complete API route for an API resource. Reverse mapping is quite useful when we share links to our web application or API. However, in order to create a URL from data, we should associate a Name with the gorilla/mux route. You can name a multiplexer route, like this:
r.HandlerFunc("/articles/{category}/{id:[0-9]+}", ArticleHandler).
Name("articleRoute")
Now, we can get a dynamically generated API route by using the url method:
url, err := r.Get("articleRoute").URL("category", "books", "id", "123")
fmt.Printf(url.Path) // prints /articles/books/123
If a route consists of the additional path parameters defined, we should pass that data as arguments to the URL method.
The next important feature of a URL router is path prefix. A path prefix is a wildcard route for matching all possible paths. It matches all the routes to the API server, post a root word. The general use case of path prefixes is a static file server. Then, when we serve files from a static folder, the API paths should match to filesystem paths to successfully return file content.
For example, if we define /static/ as a path prefix, every API route that has this root word as a prefix is routed to the handler attached.
These paths are matched:
- http://localhost:8000/static/js/jquery.min.js
- http://localhost:8000/static/index.html
- http://localhost:8000/static/some_file.extension
Using gorilla/mux's PathPrefix and StripPefix methods, we can write a static file server, like this:
r.PathPrefix("/static/").Handler(http.StripPrefix("/static/", http.FileServer(http.Dir("/tmp/static"))))
The next important feature is strict slash. A strict slash activated on a gorilla/mux router allows a URL to redirect to the same URL with / appended at the end and vice versa.
For example, let us say we have an /articles/ route that is attached to an ArticleHandler handler:
r.StrictSlash(true)
r.Path("/articles/").Handler(ArticleHandler)
In the preceding case, strict slash is set to true. The router then redirects even /articles (without '/' at the end) to the ArticleHandler. If it is set to false, the router treats both /articles/ and /articles as different paths.
The next important feature of a URL router is to match encoded path parameters. The gorilla/mux UseEncodedPath method can be called on a router to match encoded path parameters.
A server can receive encoded paths from a few clients. We can match the encoded path parameter, we can even match the encoded URL route and forward it to the given handler:
r.UseEncodedPath()
r.NewRoute().Path("/category/id")
This can match the following URL:
http://localhost:8000/books/2
As well as this:
http://localhost:8000/books%2F2
Where %2F2 stands for /2 in encoded form.
We are free to define routes for our application. Since routes are entry points to any API, developers should be careful about how they process the data received from a client. Clients can be attackers too, who can inject malicious scripts into the path or query parameters. That situation is called a security vulnerability. APIs are prone to a common application vulnerability called SQL injection. In the next section, we introduce it briefly and see possible countermeasure steps.
SQL injection in URLs and ways to avoid them
SQL injection is a process of attacking a database with malicious scripts. If one is not careful when defining URL routes, there may be an opportunity for SQL injection. These attacks can happen for all kinds of REST operations. For example, if we are allowing the client to pass parameters to the server, then there is a chance for an attacker to append an ill-formed string to those parameters. If we are using those variables/parameters directly into an SQL query executing on our database, it could lead to a potential vulnerability.
Look at the following Go code snippet that inserts username and password details into the database. It collects values from an HTTP POST request and appends raw values to the SQL query:
username := r.Form.Get("id")
password := r.Form.Get("category")
sql := "SELECT * FROM article WHERE id='" + username + "' AND category='" + password + "'"
Db.Exec(sql)
In the snippet, we are executing a database SQL query, but since we are appending the values directly, we may include malicious SQL statements such as -- comments and ORDER BY n range clauses in the query:
?category=books&id=10 ORDER BY 10--
If the application returns the database response directly to the client, it can leak information about the columns the table has. An attacker can change the ORDER BY to another number and extract sensitive information:
Unknown column '10' in 'order clause'
We will see more about this in our upcoming chapters where we build fully-fledged REST services with other methods, such as POST, PUT, and so on:
Now, how to avoid these injections. There are several precautions:
- Set the user level permissions to various tables in the database
- Log the requests and find the suspicious ones
- Use the HTMLEscapeString function from Go's text/template package to escape special characters in the API parameters, such as body and path
- Use a driver program instead of executing raw SQL queries
- Stop relaying database debug messages back to the client
- Use security tools such as sqlmap to find out vulnerabilities
With the basics of routing and security covered, in the next section we present an interesting challenge for the reader. It is to create a URL shortening service. We provide all the background details briefly in the next section.
Reader's challenge – an API for URL shortening
With all the basics you have learned up to now, try to implement a URL shortening service. A URL shortener takes a very long URL and returns a shortened, crisp, and memorable URL back to the user. At first sight, it looks like magic, but it is a simple math trick.
In a single statement, URL shortening services are built upon two things:
- A string mapping algorithm to map long strings to short strings (Base 62)
- A simple web server that redirects a short URL to the original URL
There are a few obvious advantages of URL shortening:
- Users can remember the URL; easy to maintain
- Users can use the links where there are restrictions on text length, for example, Twitter
- Predictable shortened URL length
Take a look at the following diagram:
Under the hood, the following things happen in a URL shortening service:
- Take the original URL
- Apply BASE62 encoding on it; it generates a Shortened URL
- Store that URL in the database. Map it to the original URL ([shortened_url: original_url])
- Whenever a request comes to the shortened URL, just do an HTTP redirect to the original URL
We will implement a full example in upcoming chapters when we integrate databases to our API server, but before that, though, we should specify the API design documentation.
Take a look at the following table:
URL | REST Verb | Action | Success | Failure |
/api/v1/new | POST | Create a shortened URL | 200 | 500, 404 |
/api/v1/:url | GET | Redirect to original URL | 301 | 404 |
Summary
In this chapter, we first introduced the HTTP router. We tried to create HTTP routes using Go's net/http package. Then, we briefly discussed ServeMux with an example. We saw how to add multiple handler functions to multiple routes. Then, we introduced a lightweight router package called httprouter, which allows developers to create elegant routes, with the option of parsing parameters passed in the URL path.
We can also serve files over the HTTP using httprouter. We built a small service to get the Go version and file contents (read-only). That example can be extended to fetch any system information or run a system command.
Next, we introduced the popular Go routing library, gorilla/mux. We discussed how it is different from httprouter and explored its functionality by implementing two examples. We explained how Vars can be used to get path parameters and r.URL.Query to parse query parameters.
As part of securing API routes, we discussed SQL injection and how it can happen in our applications. We have also seen the counter measures. By the end of this chapter, one can define routes and handler functions to accept HTTP API requests.
In the next chapter, we will look at Middleware functions, which act as tamperers for HTTP requests and responses. That phenomenon helps us to modify the API response on the fly. The next chapter also features Remote Procedure Call (RPC).