Pattern Routing

Previously we created an executable package to register two handlers at fixed url patterns and start a simple HTTP server with an Address and Handler.

In this section, we’ll start to use a ServeMux that allows variable parts in url patterns and stub out the routes we’d like the application to have.

Choosing a Muxer

Many web apps handle requests to URLs with variable parts such as /settings/godric or /notes/42. ServeMux handles HTTP requests by multiplexing over handlers registered to fixed patterns only, but remember that ServeMux is just one implementation of the Handler interface. net/http wisely defines the foundational Server and Request, but leaves handling to anything that implements ServeHTTP(ResponseWriter, *Request).

Several HTTP muxers (commonly called ‘routers’) allow patterns with variable parts (called ‘parameters’). For Gopherpad, we seek the following features in a muxer:

  • Patterns with parameters and easy retrieval of their values
  • HTTP method matching rules
  • Route reversal to build redirection URLs from a handler

and we would prefer the muxer provide drop-in compatability with ServeMux.

For Gopherpad, we’ll use gorilla/pat since it is the simplest library with the desired features.

Note

I’ll mention the existence of dghubble/warp (by the author) which offers the features above, a pat-style interface, and (unlike the others) is a drop-in compatible fork of http.ServeMux.

Add the github.com/gorilla/pat import to app.go. Instead of registering pattern, handler pairs on http’s DefaultServeMux, register them on a declared pat Router.

import (
    ...
    "github.com/gorilla/pat"
)

// ServeMux node
var mux *pat.Router = pat.New()

func init() {
    mux.Get("/notes", noteHandler)
    mux.Get("/", helloHandler)
}

func main() {
    ...
    err := http.ListenAndServe(address, mux)
    ...
}

gorilla/pat provides registration methods like Get(pattern string, h http.HandlerFunc) (and similar for Post, Put, Delete) instead of ServeMux’s Handle(pattern string, handler Handler) and HandleFunc. Re-run the server and see that it works as it did before, except the handlers will only be called for GET requests.

Notice that we registered “/notes” before the catch all “/”. That’s because pat checks patterns in the order they are registered, unlike ServeMux. Muxers, even limited to just fixed patterns, can have subtle differences in how URLs match to patterns (typically in regard to priority ordering or slash redirection). There are further quirks in pat, mux, and httprouter due to their different ideas about parameters (e.g. should /api/v1/woops match /{username}? if so, what is the value of username?). A good approach is to consider ServeMux’s behavior as the standard where applicable and check how your muxer behaves.

Now let’s replace the toy routes helloHandler and noteHandler and take advantage of pat’s pattern parameters.

Route Design

Gopherpad will have routes for users to signup, login, and logout as well as a profile page for each user showing notes and a note detail page for viewing an individual note.

_images/routes-outline.png

It will also have a tiny note API under /api/v1, supporting the list, create, read, update, and delete actions.

HTTP Verb Path Action Description
GET /notes list list multiple notes
POST /notes create create a note
GET /notes/{id} read get a note
PUT /notes/{id} update update a note
DELETE /notes/{id} delete delete a note

Note

If you’re worried (and you should be) about maintaining all these routes in app.go, you’re absolutely right. What about testable MVC components? Leveraging re-usable components to avoid reimplementing user flow? Splitting the API into a separate executable easily? Refactoring the Project Structure is the topic of the next section.

Adding Handlers

Add stub handler functions for the Gopherpad routes for note browsing, login flow, and the API.

// ServeMux node
var mux *pat.Router = pat.New()

// init registers patterns and handlers on mux
func init() {
    // api
    mux.Post("/api/v1/notes", noteCreate)
    mux.Get("/api/v1/notes/{id}", noteRead)
    mux.Put("/api/v1/notes/{id}", noteUpdate)
    mux.Delete("/api/v1/notes/{id}", noteDelete)
    // login
    mux.Get("/signup", signupHandler)
    mux.Get("/login", loginHandler)
    mux.Get("/logout", logoutHandler)
    // frontend
    mux.Get("/notes/{id}", noteDetailHandler)
    mux.Get("/profile", profileHandler)
    // default
    mux.Get("/", defaultHandler)
}

// main starts serving the web application
func main() {
   ...
}

func signupHandler(w http.ResponseWriter, request *http.Request) {
    fmt.Fprintf(w, "signup")
}

func loginHandler(w http.ResponseWriter, request *http.Request) {
    fmt.Fprintf(w, "login")
}

func logoutHandler(w http.ResponseWriter, request *http.Request) {
    fmt.Fprintf(w, "logout")
}

func profileHandler(w http.ResponseWriter, request *http.Request) {
    fmt.Fprintf(w, "profile")
}

func noteDetailHandler(w http.ResponseWriter, request *http.Request) {
    noteId := request.URL.Query().Get(":id")
    fmt.Fprintf(w, "note %s detail", noteId)
}

func defaultHandler(w http.ResponseWriter, request *http.Request) {
    http.NotFound(w, request)
}

// API

func noteList(w http.ResponseWriter, request *http.Request) {
    fmt.Fprintf(w, "note list")
}

func noteCreate(w http.ResponseWriter, request *http.Request) {
    fmt.Fprintf(w, "note create")
}

func noteRead(w http.ResponseWriter, request *http.Request) {
    noteId := request.URL.Query().Get(":id")
    fmt.Fprintf(w, "read note %s", noteId)
}

func noteUpdate(w http.ResponseWriter, request *http.Request) {
    noteId := request.URL.Query().Get(":id")
    fmt.Fprintf(w, "update note %s", noteId)
}

func noteDelete(w http.ResponseWriter, request *http.Request) {
    noteId := request.URL.Query().Get(":id")
    fmt.Fprintf(w, "delete note %s", noteId)
}

Now for routes with an {id} parameter, gorilla/pat will capture the value from the URL and make it available in the URL Values map, accessible through someURL.Query().

Re-run the server and visit the following URLs to check that the GET routes return the expected message:

and curl the POST, PUT, and DELETE routes:

$ curl -X POST http://localhost:8080/api/v1/notes      # note create
$ curl -X PUT http://localhost:8080/api/v1/notes/3     # update note 3
$ curl -X DELETE http://localhost:8080/api/v1/notes/3  # delete note 3

We’ll add tests to do these checks automatically in Routing Tests, but first let’s refactor the Project Structure to meet some of our original design goals.