I was recently working on a project where I found myself wanting to set up some custom middleware based on the prefix path of a route. I didn’t see a lot of great resources explaining how to do this, so this is my attempt at explaining it.
Unfortunately I can’t share my real code, but what I can do is pretend that we are building an app and api to help people compress things. To start, let’s look at a few of the routes we expect our app to have.
/blog
- This is where we write about how we do cool technical stuff.
/pricing
- This is where we explain very clearly (or not so clearly) how much we charge for our services.
/dashboard/*
- There are going to be several routes under the dashboard prefix, and this is where we will be filtering out unauthenticated users with some middleware.
/api/*
- Finally we have our API routes. We have a few power users who apparently have a lot of files that need compressed, so we need to give them a way to send us API requests. We might authenticate these users via a header instead of a cookie.
Now that we have the gist of our routes, let’s look at some of the libraries we will be using.
We are going to use:
You can easily apply the technique we will be using to other libraries, but for simplicity’s sake we are going to use specific packages so that we aren’t limited to pseudo-code.
Lets start by coding our routes up quickly.
func main() {
r := mux.NewRouter().StrictSlash(false)
r.HandleFunc("/blog", blog)
r.HandleFunc("/pricing", pricing)
dash := r.PathPrefix("/dashboard").Subrouter()
dash.HandleFunc("/", dashboardIndex)
dash.HandleFunc("/things", dashboardThings)
dash.HandleFunc("/things/compress", dashboardCompressAllTheThings)
api := r.PathPrefix("/api").Subrouter()
api.HandleFunc("/things", apiThings)
api.HandleFunc("/things/compress", apiCompressAllTheThings)
http.ListenAndServe(":3000", r)
}
We don’t have any http.Handler
implementations here, but assume that all of the functions passed into the HandleFunc
functions are of the type http.HandlerFunc.
First, lets get a classic negroni instance created and apply this to all of our routes. This handles things like logging and handling panic recovery.
func main() {
// ... this is all roughly the same code from our
// last example
n := negroni.Classic()
n.UseHandler(r)
http.ListenAndServe(":3000", n)
}
The last three lines are the only changed/new ones in the main function. We created an instance of negroni.Negroni
using negroni.Classic
, then told it to use our mux.Router
as a handler, and finally started a server listening on port 3000 with our negroni handler.
Before we can move on we need to write some middleware for our other routes. Remember, we need to verify that users are logged in for each of our different types of endpoints.
func DashboardMiddleware(w http.ResponseWriter, r *http.Request, next http.HandlerFunc) {
// Verify that the user is logged in w/ a valid cookie
validCookie := true
if validCookie {
next(w, r)
} else {
// redirect to login
http.Redirect(w, r, "/login", http.StatusFound)
}
}
func APIMiddleware(w http.ResponseWriter, r *http.Request, next http.HandlerFunc) {
// Verify that the user is logged in w/ a valid header
validHeader := true
if validHeader {
next(w, r)
} else {
// redirect to login
http.Redirect(w, r, "/login", http.StatusFound)
}
}
Okay, so obviously I’m not about to actually write up all that code, but let’s pretend that we did.
This is where the magic happens, and it is also where things get confusing.
The easiesy way I have found to specify middleware for a path prefix is to setup a second muxer (we use the sirMuxalot
variable for ours below) that has the path prefixes we want to apply middleware to, and to then pass in our original router wrapped in some middleware for those routes.
This works because the sirMuxalot
router won’t ever call the middleware-wrapped router unless the path prefix we define gets matched with the incoming web request’s path.
sirMuxalot := http.NewServeMux()
sirMuxalot.Handle("/", r)
sirMuxalot.Handle("/api/", negroni.New(
negroni.HandlerFunc(APIMiddleware),
negroni.Wrap(r),
))
sirMuxalot.Handle("/dashboard/", negroni.New(
negroni.HandlerFunc(DashboardMiddleware),
negroni.Wrap(r),
))
n := negroni.Classic()
n.UseHandler(sirMuxalot)
http.ListenAndServe(":3000", n)
It probably seems weird to wrap our router in multiple different middleware, but this works perfectly fine and is one of the more elegant solutions to the problem.
And that’s it. If the code still seems a bit fuzzy I recommend playing around with it a bit. Again, you don’t need to use negroni or any other specific middleware for this to work, so feel free to use custom code or different libraries.
Sign up for my mailing list and I'll send you a FREE sample from my course - Web Development with Go. The sample includes 19 screencasts and the first few chapters from the book.
You will also receive emails from me about Go coding techniques, upcoming courses (including FREE ones), and course discounts.
Jon Calhoun is a full stack web developer who teaches about Go, web development, algorithms, and anything programming. If you haven't already, you should totally check out his Go courses.
Previously, Jon worked at several statups including co-founding EasyPost, a shipping API used by several fortune 500 companies. Prior to that Jon worked at Google, competed at world finals in programming competitions, and has been programming since he was a child.
Related articles
Spread the word
Did you find this page helpful? Let others know about it!
Sharing helps me continue to create both free and premium Go resources.
Want to discuss the article?
See something that is wrong, think this article could be improved, or just want to say thanks? I'd love to hear what you have to say!
You can reach me via email or via twitter.
©2024 Jonathan Calhoun. All rights reserved.