Editor’s note: This post was updated on 15 February 2023 to include information about matching routes, concurrency, as well as HttpRouter.
HTTP routers are tools and libraries that help receive and relay network requests to the specified handlers. HTTP routers run on servers, intercept incoming requests, and designate requests to specified handler functions.
Routers vary between backend frameworks; most backend frameworks ship with routers and many other functionalities for building software faster.
The Gorilla Mux package is one of the most popular routers and projects in the Go ecosystem, used in popular projects like Geth because of the package’s diverse features. Gorilla Mux provides functionalities for matching routes, serving static files, serving single-page applications (SPAs), middleware, handling CORS requests, and testing handlers.
N.B., as of December 2022, the entire Gorilla Web Toolkit was put into archive mode on GitHub. In the words of the project maintainers, this means that:
“All the repositories have gone into “read-only” mode. Anyone still using them can still clone them, go get them, and continue to build projects against them. In effect, there’s really no change here from the last 12 months, and it won’t break existing projects.
…What it does signal is that there will be no future development on these libraries.
…Folks are welcome to (as they always have been) fork them: all of the Gorilla libraries are permissively licensed (MIT, BSD-3, and Apache 2.0).”
You can read further details about why the toolkit was archived here.
This tutorial will walk you through using the Gorilla Mux package as a router for your applications. You’ll learn how to work with Gorilla Mux by using it to build a simple API.
Jump ahead:
The Replay is a weekly newsletter for dev and engineering leaders.
Delivered once a week, it's your curated guide to the most important conversations around frontend dev, emerging AI tools, and the state of modern software.

Once you’ve set up your Go workspace, run this command in your working directory to install the Gorilla Mux package:
go get -u github.com/gorilla/mux
After installing the Gorilla Mux package, import the packages and modules that you’ll be using in this tutorial at the top of your Go file, like so:
import (
"encoding/json"
"github.com/gorilla/mux"
"log"
"net/http"
)
Gorilla Mux depends on the standard http package, and you’ll use the http package in many parts of this tutorial, including setting up a server. You’ll use the json package to encode and decode structs to JSON and vice versa.
Here’s the struct type you’ll use as the data model in this tutorial:
type Bio struct {
Name string `json:"name"`
Age int `json:"age"`
}
Gorilla Mux doesn’t provide functionality for parsing structs to JSON the way frameworks like Fiber do. Instead, in this tutorial you’ll be using the json package from the standard library to decode JSON requests and encode structs as JSON responses to clients.
Here’s an example of encoding and decoding with the json package. Start by creating the Go variable you want to decode:
var human Bio
The human variable is an instantiation of the Bio struct. You can use the Decode method of the Decoder struct returned by json.NewDecoder to parse the JSON body of an HTTP request into the initialized struct.
err := json.NewDecoder(request.Body).Decode(&human)
if err != nil {
log.Fatalln("There was an error decoding the request body into the struct")
}
Similarly, you can use the Encode method of the Encoder struct returned by NewEncoder to write a struct that will be encoded as JSON to the client.
err = json.NewEncoder(writer).Encode(&human)
if err != nil {
log.Fatalln("There was an error encoding the initialized struct")
}
Now that you’re familiar with the basics of working with JSON, let’s move on to routing.
You can create a router instance with the NewRouter method, like so:
router := mux.NewRouter()
After declaring a new router instance, you can use the HandleFunc method of your router instance to assign routes to handler functions along with the request type that the handler function handles. Here’s an example:
router.HandleFunc("/api/v1/example", exampleHandler).Methods("GET")
The HandleFunc method assigned the api/v1/example route to the exampleHandler handler function to handle GET requests.
These are the router declarations for the endpoints of the CRUD API you’ll be building in this tutorial:
router.HandleFunc("/create", create).Methods("POST")
router.HandleFunc("/read", read).Methods("GET")
router.HandleFunc("/update", update).Methods("PUT")
router.HandleFunc("/delete", delete_).Methods("DELETE")
Your next task is to create these handler functions and set up a server.
Before you go ahead to create your handlers, let’s pause and take a closer look at how you can match routes with Gorilla Mux. Right now your routing is based on the root of the application and the HTTP request method. For example, the declaration:
router.HandleFunc("/create", create).Methods("POST")
We’ll call the create handler when a client makes a POST request to the address yourserveraddress.com/create. However, Gorilla Mux offers ways to be even more precise about which routes your endpoints match.
Let’s assume you wanted to ensure your create handler only processed requests made over HTTPS. All you’d need is to chain a scheme match to the route declaration, like so:
router.HandleFunc("/create", create).Methods("POST").Schemes("https")
Gorilla Mux provides matches for several other criteria. For example, you can match routes by host URL, like this: router.HandleFunc("/route", handler).Host("www.yourdomain.com"). This declaration will only process requests to the URL www.yourdomain.com/route .
Mux also allows you to:
You can find examples using these matches here. A word of warning about matching routes with Mux: if an incoming request matches two or more route declarations (there’s a route conflict), the request will always be processed using the first route declaration.
Because applying the same matches to several different routes is tedious and can make your code harder to read, Gorilla Mux provides a feature called subrouting. This feature allows you to create a router that will apply a set of matches to all routes you register with it. Here’s an example:
router := mux.NewRouter()
s := router.Host("www.yourdomain.com").Headers("Connection", "Keep-Alive").Subrouter()
s.HandleFunc("/create", handler)
s.HanldeFunc("/update", updateHandler)
The /create and /update routes will now only be processed if the request has the appropriate Host and Headers. Now that you understand how route matching works, you can move on to creating your handlers.
Your handler functions are where you’ll declare the business logic for your application. Depending on the operation, your handlers will need a writer object (to write a response to the client) and a request object (to obtain information about the incoming request).
These objects will typically be an instance of the http.ResponseWriter and/or *http.Request types, respectively. Here’s a skeletal example of a typical handler function that returns JSON responses to the client:
func example(writer http.ResponseWriter, request *http.Request) {
writer.Header().Set("Content-Type", "application/json")
}
Your next step is to create the data store thatyou’ll use for this tutorial:
var BioData = make([]Bio, 0)
The BioData variable above is a slice of the Bio type you defined earlier. This data store is sufficient for this tutorial, but your projects will likely have more sophisticated needs that require you to use a database. If you aren’t familiar with working with databases in Go, you can check out the tutorials on MongoDB and the GORM ORM.
Now that your data store is in place, you can begin creating your request handlers. The create handler function is assigned to process POST requests, so its business logic will save the new JSON object in the request body to the application’s data store. Here’s the code for the create function:
func create(writer http.ResponseWriter, request *http.Request) {
writer.Header().Set("Content-Type", "application/json")
writer.WriteHeader(http.StatusOK)
var human Bio
err := json.NewDecoder(request.Body).Decode(&human)
if err != nil {
log.Fatalln("There was an error decoding the request body into the struct")
}
BioData = append(BioData, human)
err = json.NewEncoder(writer).Encode(&human)
if err != nil {
log.Fatalln("There was an error encoding the initialized struct")
}
}
The create handler function writes the StatusOk header to the client on request receipt, decodes the JSON request body into the human struct instance, saves the human struct to the BioData slice, and ends by writing the human struct as a response to the client.
Next up is the read handler. The read handler function is assigned to GET requests; thus, its business logic will fetch data from the data store and return matching data to the client based on the client’s request:
func read(writer http.ResponseWriter, request *http.Request) {
writer.Header().Set("Content-Type", "application/json")
name := mux.Vars(request)["name"]
for _, structs := range BioData {
if structs.Name == name {
err := json.NewEncoder(writer).Encode(&structs)
if err != nil {
log.Fatalln("There was an error encoding the initialized struct")
}
}
}
}
The read function works by reading the name parameter of the request using the Vars method of the mux package. It then loops through the BioData slice you’re using as a data store, and returns the struct that matches the name parameter to the client as JSON.
The update handler function is assigned to PUT requests, so its business logic should update the struct in the BioData data store specified by the request:
func update(writer http.ResponseWriter, request *http.Request) {
writer.Header().Set("Content-Type", "application/json")
var human Bio
err := json.NewDecoder(request.Body).Decode(&human)
if err != nil {
log.Fatalln("There was an error decoding the request body into the struct")
}
for index, structs := range BioData {
if structs.Name == human.Name {
BioData = append(BioData[:index], BioData[index+1:]...)
}
}
BioData = append(BioData, human)
err = json.NewEncoder(writer).Encode(&human)
if err != nil {
log.Fatalln("There was an error encoding the initialized struct")
}
}
The update function parses the JSON in the request body into the human variable, loops through the BioData slice, deletes the entry if it exists, and finally appends the human struct from the request body to the BioData slice.
The delete_ handler function is assigned to DELETE requests; thus, its business logic will delete a specified struct from the data store:
func delete_(writer http.ResponseWriter, request *http.Request) {
writer.Header().Set("Content-Type", "application/json")
name := mux.Vars(request)["name"]
indexChoice := 0
for index, structs := range BioData {
if structs.Name == name {
indexChoice = index
}
}
BioData = append(BioData[:indexChoice], BioData[indexChoice+1:]...)
}
The delete_ function retrieves the name parameter from the request, loops through the BioData data store, and deletes the entry if it exists.
Now that you’ve set up your handler functions, the last step is to set up the server that will listen for requests.
You can set up a server using the ListenAndServe method of the http package. The ListenAndServe method takes as arguments the port you want the server to run on and a router instance, if any. Here’s a server for your application:
func RunServer() {
router := mux.NewRouter()
router.HandleFunc("/create", create).Methods("POST")
router.HandleFunc("/read", read).Methods("GET")
router.HandleFunc("/update", update).Methods("PUT")
router.HandleFunc("/delete", delete_).Methods("DELETE")
err := http.ListenAndServe(":8080", router)
if err != nil {
log.Fatalln("There's an error with the server," err)
}
}
Calling the RunServer function in the main function of your project should start up a server on the local host port 8080.
And, that’s all you need to know to get started building apps with Gorilla Mux!
Using Gorilla Mux doesn’t have much impact on the concurrency of your application compared to using the standard library’s ServeMux. Each handler function will still be called in a separate goroutine when processing an incoming request.
So what does this mean for you? It means you have to be careful when modifying data that’s shared between handlers, so you avoid race conditions. The easiest way to implement this is usually to avoid having any shared data in the first place and use only local variables. However, if shared data is something you can’t avoid, be sure to guard all access to the data with a Mutex or some other synchronization mechanism.

Chi is a lightweight, composable router for building HTTP services in Go. You’ll find the Chi router useful for building large RESTful API services you want to maintain and support over time. Heroku, Cloudflare, and 99designs use the Chi router in production.
Chi is built on the context package, making it suitable for handling signaling, cancellation, and request-scoped operations across handler chains. The Chi package also contains sub-packages for middleware and generating documentation, and a rendering package for managing HTTP requests and response payloads.
Here’s a quick example of routing with the Chi router:
import (
"net/http"
"github.com/go-chi/chi/v5"
"github.com/go-chi/chi/v5/middleware"
)
func main() {
router := chi.NewRouter()
router.Use(middleware.Logger)
router.Get("/", func(writer http.ResponseWriter, request *http.Request) {
writer.Write([]byte("welcome to the chi"))
})
http.ListenAndServe(":3000", router)
}
The main function starts up a server that listens on port 3000 and writes a string to the client as a response.
When you start a new project, you might wonder which router to use. Here’s a comparison between the two router packages to help you decide based on what you’re building:
| Metric | Gorilla Mux | Chi Router |
|---|---|---|
| Speed | Fast, see benchmarks | Fast, see benchmarks |
| Documentation Generation | No | Yes |
| Popularity | 17k stars, used by 77k projects on GitHub | 12k stars, used by 11k projects on GitHub |
| Rendering | Yes | Yes |
| MiddleWare | Yes | Yes |
| WebSockets | Yes | Yes |
| Testing | Yes | Yes |
The Gorilla Mux and Chi router are both great at routing, but you’ll find most Go developers use Gorilla Mux because it’s older and because there are more learning resources for Gorilla Mux.

HttpRouter is a fast, minimal router for Go applications that’s been around for a long time. It’s so fast and reliable that it’s used as the router of several Go web frameworks, the most popular of those being Gin. Here’s what the docs say about HttpRouter:
HttpRouter is a lightweight high performance HTTP request router (also called multiplexer or just mux for short) for Go.
In contrast to the default mux of Go’s
net/httppackage, this router supports variables in the routing pattern and matches against the request method. It also scales better.The router is optimized for high performance and a small memory footprint. It scales well even with very long paths and a large number of routes. A compressing dynamic trie (radix tree) structure is used for efficient matching
To give you a feel for how it works, here’s a tiny demo of routing with HttpRouter:
import (
"github.com/julienschmidt/HttpRouter"
"log"
"net/http"
)
func main() {
router := HttpRouter.New()
router.GET("/", func(w http.ResponseWriter, r *http.Request, _ HttpRouter.Params) {
w.Write([]byte("Hello from http router"))
})
log.Fatal(http.ListenAndServe(":8080", router))
}
Just like with Chi, in this demo the main function starts up a server that listens on port 8080 and writes a hello response to the client. To help you choose the best router for your needs, here’s a comparison between HttpRouter and Mux:
| Metric | Gorilla Mux | HttpRouter |
|---|---|---|
| Speed | Fast, see benchmarks | Very fast, see benchmarks |
| Documentation Generation | No | No |
| Popularity | 17k stars, used by 77k projects on GitHub | 15k stars, used by 16k projects on GitHub |
| Rendering | Yes | Yes |
| MiddleWare | Yes | Yes |
| WebSockets | Yes | No |
| Testing | Yes | Yes |
Gorilla Mux and HttpRouter are both excellent routers. HttpRouter has the advantage of speed and minimalism, but Mux offers more flexibility and functionality.
In this tutorial, you learned about the Gorilla Mux, HttpRouter, and Chi router packages, how to route and build APIs with the Gorilla Mux router, and how to evaluate the three packages to help you make better decisions for your projects.
Check out how these Go frameworks can help you build web applications faster.
Install LogRocket via npm or script tag. LogRocket.init() must be called client-side, not
server-side
$ npm i --save logrocket
// Code:
import LogRocket from 'logrocket';
LogRocket.init('app/id');
// Add to your HTML:
<script src="https://cdn.lr-ingest.com/LogRocket.min.js"></script>
<script>window.LogRocket && window.LogRocket.init('app/id');</script>

Discover what’s new in The Replay, LogRocket’s newsletter for dev and engineering leaders, in the October 29th issue.

Learn about the new features in the Next.js 16 release: why they matter, how they impact your workflow, and how to start using them.

Test out Meta’s AI model, Llama, on a real CRUD frontend projects, compare it with competing models, and walk through the setup process.

Rosario De Chiara discusses why small language models (SLMs) may outperform giants in specific real-world AI systems.
Would you be interested in joining LogRocket's developer community?
Join LogRocket’s Content Advisory Board. You’ll help inform the type of content we create and get access to exclusive meetups, social accreditation, and swag.
Sign up now