selfie of Kenton

Kenton Vizdos

i make cool things in go. this is my dev log. note: it's not a technical masterpiece, I just like writing (sometimes poorly) about what I learn!

Routing Patterns in Go Stdlib

Modern Approaches to Routing #

Nowadays, Go developers can stay in the standard library for quite a while. There's no real need to reach for a third-party router (Chi, FastHTTP, Gorilla Mux, etc) unless you're backed by a strong technical reason like zero allocations or tight latency budgets.

In the previous post, we saw how basic routing works but ran into organizational challenges. As a reminder of what the endpoints looked like:

1func main() {
2	http.HandleFunc("GET /api/users", usersHandler)
3	http.HandleFunc("GET /api/users/{id}", userHandler)
4	http.HandleFunc("GET /api/posts", postsHandler)
5	http.HandleFunc("GET /api/posts/{id}", postHandler)
6	// ...
7}

Since most of these share a common prefix, we can, and should, start to group our endpoints:

 1func RouteAPI() *http.ServeMux {
 2	mux := http.NewServeMux()
 3	mux.HandleFunc("GET /users", usersHandler)
 4	mux.HandleFunc("GET /users/{id}", userHandler)
 5	mux.HandleFunc("GET /posts", postsHandler)
 6	mux.HandleFunc("GET /posts/{id}", postHandler)
 7	return mux
 8}
 9
10func main() {
11	http.Handle("/api/", http.StripPrefix("/api", RouteAPI()))
12	// ...
13}

With this general flow in mind, we can begin to see how this scaffolds. The API routes are now organized and only accessible via /api/, however we can still see logical grouping happening within the RouteAPI() function. Let's take this one step further...

 1func RouteUsers() *http.ServeMux {
 2	mux := http.NewServeMux()
 3	mux.HandleFunc("GET /", usersHandler)
 4	mux.HandleFunc("GET /{id}", userHandler)
 5	return mux
 6}
 7
 8func RoutePosts() *http.ServeMux {
 9	mux := http.NewServeMux()
10	mux.HandleFunc("GET /", postsHandler)
11	mux.HandleFunc("GET /{id}", postHandler)
12	return mux
13}
14
15func RouteAPI() *http.ServeMux {
16	mux := http.NewServeMux()
17	mux.Handle("/users/", http.StripPrefix("/users", RouteUsers()))
18	mux.Handle("/posts/", http.StripPrefix("/posts", RoutePosts()))
19	return mux
20}
21
22func main() {
23	http.Handle("/api/", http.StripPrefix("/api", RouteAPI()))
24	// ...
25}

This modular approach scales naturally, and it's all in the stdlib.

What Is http.StripPrefix? #

In the code above, I slipped in this critical bit of code to manage the routing:

1http.Handle("/api/", http.StripPrefix("/api", RouteAPI()))

But... what the heck is StripPrefix actually doing?

Think of it like a middleman that chops off part of the URL path before handing it off to the next mux or handler.

Example: #

If a request comes in for /api/users, and we've done this:

1http.Handle("/api/", http.StripPrefix("/api", someHandler))

Then someHandler will see the request as /users. It doesn't know (or care) that it used to start with /api:

1// A request comes into /api/users...
2http.Handle("/api/", http.StripPrefix("/api", someHandler))
3// someHandler now gets a request with the path set to `/users`

This makes it possible to compose handlers cleanly and nest routing logic without each inner mux needing to know its full path.

You might've noticed only the first argument in StripPrefix has a trailing slash: that's intentional, and required:

1http.Handle("/api/", http.StripPrefix("/api", someHandler))

This is critical so that, once passed to the handler, the "new path" will begin with a slash.

Trailing Slashes Matter #

That trailing / in /api/ or /users/? Yeah, it's not just stylistic sadly.

In http.ServeMux, only paths that end in a slash are treated as prefix matches. This means:

1// Matches /api/* — GOOD
2// GET /api/test WILL resolve
3mux.Handle("/api/", handler)
4
5 // Only matches /api exactly — probably BAD
6 // GET /api/test WILL NOT resolve
7mux.Handle("/api", handler)

So if you want your handler or sub-mux to respond to everything under /api, you need the slash.

These little things: StripPrefix, trailing slashes might seem nitpicky, but they're what let muxes scale cleanly. Once you get a feel for them, muxing in Go is dead simple.

Stdlib has your back #

With these patterns, you can build production-ready APIs that stay organized as they grow, all without leaving the standard library. The combination of nested muxes, StripPrefix, and careful attention to trailing slashes gives you everything you need for clean, scalable routing.

No third-party dependencies required.