Golang HTTP router
BunRouter is an extremely fast HTTP router for Go with unique combination of features:
- Middlewares allow to extract common operations from HTTP handlers into reusable functions.
- Error handling allows to further reduce the size of HTTP handlers by handling errors in middlewares.
- Routes priority enables meaningful matching priority for routing rules: first static nodes, then named nodes, lastly wildcard nodes.
- net/http compatible API which means using minimal API without constructing huge wrappers that try to do everything: from serving static files to XML generation (for example,
gin.Context
orecho.Context
).
Router | Middlewares | Error handling | Routes priority | net/http API |
---|---|---|---|---|
BunRouter | ✔️ | ✔️ | ✔️ | ✔️ |
httprouter | ❌ | ❌ | ❌ | ✔️ |
Chi | ✔️ | ❌ | ✔️ | ✔️ |
Echo | ✔️ | ✔️ | ❌ | ❌ |
Gin | ✔️ | ✔️ | ❌ | ❌ |
Installation
To install Golang HTTP router:
go get github.com/uptrace/bunrouter
Creating Go router
Usually, you create a router per app/domain:
import "github.com/uptrace/bunrouter"
router := bunrouter.New()
And use it to add routes:
router.GET("/", func(w http.ResponseWriter, req bunrouter.Request) error {
// req embeds *http.Request and has all the same fields and methods
fmt.Println(req.Method, req.Route(), req.Params().Map())
return nil
})
bunrouter.New
accepts a couple of options to customize the router:
WithNotFoundHandler(handler bunrouter.HandlerFunc)
overrides the handler that is called when there are no matching routes.WithMethodNotAllowedHandler(handler bunrouter.HandlerFunc)
overrides the handler that is called when a route matches, but the route does not have a handler for the requested method.Use(middleware bunrouter.MiddlewareFunc)
adds the middleware to the router's stack of middlewares. Router will apply the middleware to all route handlers includingNotFoundHandler
andMethodNotAllowedHandler
.
For example, to log requests, you can install reqlog
middleware:
import (
"github.com/uptrace/bunrouter"
"github.com/uptrace/bunrouter/extra/reqlog"
)
router := bunrouter.New(
bunrouter.Use(reqlog.NewMiddleware()),
)
Routes
You define routes by specifying a HTTP method and a routing rule, for example:
router.POST("/users", createUserHandler)
router.GET("/users/:id", showUserHandler)
router.PUT("/users/:id", updateUserHandler)
router.DELETE("/users/:id", deleteUserHandler)
Handlers
bunrouter.HandlerFunc
is a slightly enhanced version of http.HandlerFunc
that accepts a bunrouter.Request
and returns errors that you can handle with middlewares:
func handler(w http.ResponseWriter, req bunrouter.Request) error {
return nil
}
bunrouter.Request
is a thin wrapper over *http.Request
with route name and params:
type Request struct {
*http.Request
params Params
}
bunrouter.HandlerFunc
implements http.Handler
interface and can be used with standard HTTP middlewares. BunRouter also provides the following helpers to work with bunrouter.HandlerFunc
:
- bunrouter.HTTPHandler converts
http.Handler
tobunrouter.HandlerFunc
. - bunrouter.HTTPHandlerFunc converts
http.HandlerFunc
tobunrouter.HandlerFunc
.
For example, to use a classical HTTP handler with BunRouter:
router.GET("/", bunrouter.HTTPHandlerFunc(func(w http.ResponseWriter, req *http.Request) {
// ...
}))
Compat handlers
Using compatibility API, you can directly work with http.HandlerFunc
handlers, for example:
router := bunrouter.New().Compat()
router.GET("/", func(w http.ResponseWriter, req *http.Request) {
params := bunrouter.ParamsFromContext(req.Context())
fmt.Println(params.Route(), params.Map())
})
Verbose handlers
BunRouter also supports httprouter-like handlers, for example:
router := bunrouter.New().Verbose()
router.GET("/", func(w http.ResponseWriter, req *http.Request, ps bunrouter.Params) {
fmt.Println(params.Route(), params.Map(), params.ByName("param"))
})
Routing rules
Routing rule is a path that contains zero or more routing params, for example, /users/:id
or /image/*path
.
BunRouter supports the following param types:
:param
is a named parameter that matches a single path segment (text between slashes).*param
is a wildcard parameter that matches everything and must always be at the end of the route.
Route: /static
/static match
/static/ redirect to /static
Route: /static/
/static redirect to /static/
/static/ match
Route: /users/:id
/users no match
/users/ no match
/users/123 match
/users/foo match
/users/foo-bar match
/users/foo/bar no match
Route: /static/*path
/static redirect to /static/
/static/ match
/static/foo match
/static/foo-bar match
/static/foo/bar match
You can retrieve the matched route params using Params method:
func handler(w http.ResponseWriter, req bunrouter.Request) error {
params := req.Params()
path := params.ByName("path")
id, err := params.Int64("id")
}
Or using compatibility API:
func handler(w http.ResponseWriter, req *http.Request) {
params := bunrouter.ParamsFromContext(req.Context())
path := params.ByName("path")
id, err := params.Int64("id")
}
Routes priority
Routing rules have matching priority that is based on node types and does not depend on routes definition order:
- Static nodes, for example,
/users/
- Named nodes, for example,
:id
. - Wildcard nodes, for example,
*path
.
The following routes are sorted by their matching priority from the highest to the lowest:
/users/list
./users/:id
./users/*path
.
Routing groups
You can add routes using the router
:
router.GET("/api/users/:id", handler)
router.POST("/api/users", handler)
router.PUT("/api/users/:id", handler)
router.DELETE("/api/users/:id", handler)
But it is better to group routes by functionality under some prefix:
group := router.NewGroup("/api/users")
group.GET("/:id", handler)
group.POST("", handler)
group.PUT("/:id", handler)
group.DELETE("/:id", handler)
Or even better:
router.WithGroup("/api/users", func(group *bunrouter.Group) {
group.GET("/:id", handler)
group.POST("", handler)
group.PUT("/:id", handler)
group.DELETE("/:id", handler)
})
You can also nest groups to build complex APIs:
router.WithGroup("/api/categories", func(group *bunrouter.Group) {
// /api/categories/:category/items
group.WithGroup("/:category/items", func(group *bunrouter.Group) {})
// /api/categories/archive
group.WithGroup("/archive", func(group *bunrouter.Group) {})
})
Groups can even have their own middlewares to further customize request processing.
Not found handler
To customize not found handler:
router := bunrouter.New(
bunrouter.WithNotFoundHandler(notFoundHandler),
)
func notFoundHandler(w http.ResponseWriter, req bunrouter.Request) error {
w.WriteHeader(http.StatusNotFound)
fmt.Fprintf(
w,
"<html>BunRouter can't find a route that matches <strong>%s</strong></html>",
req.URL.Path,
)
return nil
}
Method not allowed handler
To customize method not found handler:
router := bunrouter.New(
bunrouter.WithMethodNotAllowedHandler(methodNotAllowedHandler),
)
func methodNotAllowedHandler(w http.ResponseWriter, req bunrouter.Request) error {
w.WriteHeader(http.StatusMethodNotAllowed)
fmt.Fprintf(
w,
"<html>BunRouter does have a route that matches <strong>%s</strong>, "+
"but it does not handle method <strong>%s</strong></html>",
req.URL.Path, req.Method,
)
return nil
}
Testing routes
BunRouter exposes ServeHTTPError
method that serves the request returning the error from the matched route handler:
router := bunrouter.New()
router.GET("/user/:param", func(w http.ResponseWriter, req bunrouter.Request) error {
require.Equal(t, "hello", req.Param("param"))
return nil
})
w := httptest.NewRecorder()
req, _ := http.NewRequest(http.MethodGet, "/user/hello", nil)
err := router.ServeHTTPError(w, req)
require.NoError(t, err)