rux
Simple and fast web framework for build golang HTTP applications
About rux
This large README is shown as a plain-text preview. Read the full README at the source
# Rux

[](https://github.com/gookit/rux/actions)
[](https://github.com/gookit/rux)
[](https://pkg.go.dev/github.com/gookit/rux?tab=doc)
[](https://coveralls.io/github/gookit/rux?branch=master)
[](https://goreportcard.com/report/github.com/gookit/rux)
Simple and fast web framework for build golang HTTP applications.
> [中文说明](README.zh-CN.md)
## v2 Highlights
`rux` v2 is a clean-room rewrite focused on extreme performance:
- High-performance Radix Tree routing — per-method tree, lock-free hot path
- Zero-allocation static routes (one map lookup per request)
- Inline `Params [16]Param` in `Context` for low-allocation dynamic routing
- Auto-freeze on first `ServeHTTP` — the routing tables become read-only at runtime
- Auto HEAD → GET mirror at freeze time (no manual `r.HEAD` boilerplate)
- Pre-merged middleware chains (no per-request `append`)
- Same high-level API as v1 — `Router`, `Group`, `Resource`, `Controller`,
`GET/POST/...` all unchanged.
See [_benchmarks/v2-results.txt](_benchmarks/v2-results.txt) for measured numbers, and [docs/MIGRATION-v1-to-v2.md](docs/MIGRATION-v1-to-v2.md) for breaking changes.
## Features
**Routing & request handling**
- Fast route match (Radix Tree, per-method, lock-free hot path), route groups
- Path params and named routing
- Route / group / global middleware
- `RESTful` and `Controller` style structs
- Generic `http.Handler` middleware
- Static file / directory / `embed.FS` serving
- `NotFound`, `NotAllowed`, `Error`, and panic handlers
**Built-in batteries (`server/`, `pkg/*`)**
- Production-ready Server with sane timeouts, graceful shutdown, lifecycle hooks, and `/healthz` + `/readyz` endpoints ([docs](docs/echo-server.md))
- Echo Server: httpbin-style debug endpoints (`/anything`, `/status/{code}`, `/delay`, `/redirect`, `/cookies`,
`/basic-auth`, `/bytes`, `/uuid`, `/download`, `/upload`, …)
- Render package (`pkg/render`): stateless helpers + status-aware `Responder` for JSON / XML / Text / HTML / Binary / Auto, with a
pluggable `TemplateRenderer` interface (no template engine bundled)
- Server-Sent Events (`pkg/sse`): `Stream` / `StreamWith` with lifecycle hooks, default `:connected` frame, optional keepalive,
plus a `Hub` for keyed push and broadcast to active clients
## GoDoc
- [godoc for github](https://pkg.go.dev/github.com/gookit/rux?tab=doc)
## Install
```bash
go get github.com/gookit/rux/v2
```
## Quick start
```go
package main
import (
"fmt"
"github.com/gookit/rux/v2"
)
func main() {
r := rux.New()
// Add Routes:
r.GET("/", func(c *rux.Context) {
c.Text(200, "hello")
})
r.GET("/hello/{name}", func(c *rux.Context) {
c.Text(200, "hello "+c.Param("name"))
})
r.POST("/post", func(c *rux.Context) {
c.Text(200, "hello")
})
// add multi method support for a route path
r.Add("/post[/{id}]", func(c *rux.Context) {
if c.Param("id") == "" {
// do create post
c.Text(200, "created")
return
}
id := c.Params().Int("id")
// do update post
c.Text(200, "updated "+fmt.Sprint(id))
}, rux.POST, rux.PUT)
// Start server
r.Listen(":8080")
// can also
// http.ListenAndServe(":8080", r)
}
```
### Random port and the resolved address
`Listen` binds before it serves, so with an OS-assigned port (`":0"`,
`"127.0.0.1:0"`) the address it prints and reports is the real one. Use `Bind`
when you need the address before traffic starts, then hand the listener to
`ServeListener` (which blocks):
```go
r := rux.New()
r.GET("/", home)
ln, err := r.Bind("127.0.0.1:0") // or Bind(":0") / Bind() for the default
if err != nil {
log.Fatal(err)
}
fmt.Println("listening on", r.ListenAddr()) // 127.0.0.1:50447
fmt.Println("port", r.ListenPort()) // 50447
r.ServeListener(ln) // blocks; also accepts a listener you created yourself
```
`Listener()` returns the active listener (nil once the server exits) and `Err()`
reports the serve error. For graceful shutdown, health endpoints, lifecycle hooks
and a browser-ready `LocalURL()`, use the `server` package instead (see
[Production-Ready Server](#production-ready-server)).
## Route Group
Two forms with the same semantics (prefixes concatenate, middleware stacks):
```go
// closure form: routes are registered on the outer router
r.Group("/articles", func() {
r.GET("", func(c *rux.Context) {
c.Text(200, "view list")
})
r.POST("", func(c *rux.Context) {
c.Text(200, "create ok")
})
r.GET(`/{id}`, func(c *rux.Context) {
c.Text(200, "view detail, id: "+c.Param("id"))
})
}, auth())
// value form (gin-style)
api := r.NewGroup("/api", auth())
api.GET("/users", listUsers) // GET /api/users, auth runs first
admin := api.NewGroup("/admin", isAdmin())
admin.DELETE("/users/{id}", deleteUser) // DELETE /api/admin/users/{id}
```
Inside a request the order is `global -> group (outer to inner) -> route -> handler`.
`g.Use(mw)` adds middleware for routes registered after that call, `g.Prefix()` and
`g.Router()` expose the group context, and a group created inside a closure group
inherits its prefix and middleware. Like every other registration call, group
routes must be added before the first request.
## Path Params
In v2 the path-param syntax is `{name}` (named) or `*name` (wildcard).
> Regex constraints such as `{id:\d+}` are no longer supported — validate inside the handler or with `handlers.ParamRegex` (see the migration guide).
```go
// can access by: "/blog/123"
r.GET(`/blog/{id}`, func(c *rux.Context) {
id := c.Params().Int("id")
if id <= 0 {
c.AbortWithStatus(400)
return
}
c.Text(200, fmt.Sprintf("view detail, id: %d", id))
})
// or reject non-matching ids before the handler runs:
r.GET(`/users/{id}`, showUser, handlers.ParamRegex("id", `\d+`)) // 400 on "abc"
```
optional params, like `/about[.html]` or `/posts[/{id}]`:
```go
// can access by: "/blog/my-article" or "/blog/my-article.html"
r.GET(`/blog/{title}[.html]`, func(c *rux.Context) {
c.Text(200, "view detail, title: "+c.Param("title"))
})
r.Add("/posts[/{id}]", func(c *rux.Context) {
if c.Param("id") == "" {
// do create post
c.Text(200, "created")
return
}
id := c.Params().Int("id")
// do update post
c.Text(200, "updated "+fmt.Sprint(id))
}, rux.POST, rux.PUT)
```
### Wildcards
Catch-all wildcards capture everything past the prefix:
```go
r.GET("/files/*path", func(c *rux.Context) {
c.Text(200, "serve: "+c.Param("path"))
})
```
## Use Middleware
rux support use middleware, allow:
- global middleware
- group middleware
- route middleware
**Call priority**: `global middleware -> group middleware -> route middleware`
Fallback responses use the same order: the `NotFound` (404) and `NotAllowed` (405) handlers run **after** the global middleware chain, so authentication, security headers and request logging also cover unknown paths and method mismatches. Like `Use`, both must be registered before the first request (the composed chains are built when the router freezes).
`Use` may be called any time before the first request: global middleware is merged
into every route chain when the router freezes, so a `Use` call also covers routes
that were registered before it. Declaring it before your routes is still the
clearest style (and the only option after the first request, which panics). Note
that this makes `Use` inside a `Group` closure global, not group-scoped; pass
middleware to `Group` for group scope.
Examples:
```go
package main
import (
"fmt"
"github.com/gookit/rux/v2"
)
func main() {
r := rux.New()
// add global middleware
r.Use(func(c *rux.Context) {
// do something ...
})
// add middleware for the route
route := r.GET("/middle", func(c *rux.Context) { // main handler
c.WriteString("-O-")
}, func(c *rux.Context) { // middle 1
c.WriteString("a")
c.Next() // Notice: call Next()
c.WriteString("A")
// if call Abort(), will abort at the end of this middleware run
// c.Abort()
})
// add more by Use()
route.Use(func(c *rux.Context) { // middle 2
c.WriteString("b")
c.Next()
c.WriteString("B")
})
// now, access the URI /middle
// will output: ab-O-BA
}
```
- **Call sequence**: `middle 1 -> middle 2 -> main handler -> middle 2 -> middle 1`
- **Flow chart**:
```text
+-----------------------------+
| middle 1 |
| +----------------------+ |
| | middle 2 | |
start | | +----------------+ | | end
------->| | | main handler | | |--->----
| | |________________| | |
| |______________________| |
|_____________________________|
```
> more please see [middleware_test.go](middleware_test.go) middleware tests
## Use http.Handler
rux is support generic `http.Handler` interface middleware
> You can use `rux.WrapHTTPHandler()` convert `http.Handler` as `rux.HandlerFunc`
```go
package main
import (
"net/http"
"github.com/gookit/rux/v2"
// here we use gorilla/handlers, it provides some generic handlers.
"github.com/gorilla/handlers"
)
func main() {
r := rux.New()
// create a simple generic http.Handler
h0 := http.HandlerFunc(func (w http.ResponseWriter, r *http.Request) {
w.Header().Set("new-key", "val")
})
r.Use(rux.WrapHTTPHandler(h0), rux.WrapHTTPHandler(handlers.ProxyHeaders()))
r.GET("/", func(c *rux.Context) {
c.Text(200, "hello")
})
// add routes ...
// Wrap our server with our gzip handler to gzip compress all responses.
http.ListenAndServe(":8000", handlers.CompressHandler(r))
}
```
## More Usage
### Static Assets
```go
package main
import (
"embed"
"net/http"
"github.com/gookit/rux/v2"
)
//go:embed static
var embAssets embed.FS
func main() {
r := rux.New()
// one file
r.StaticFile("/site.js", "testdata/site.js")
// allow any files in the directory.
r.StaticDir("/static", "testdata")
// file type limit in the directory
r.StaticFiles("/assets", "testdata", "css|js")
// go 1.16+: use embed assets. access: /embed/static/some.html
r.StaticFS("/embed", http.FS(embAssets))
}
```
### Name Route
In `rux`, you can add a named route, and you can get the corresponding route instance(`rux.Route`) from the router according to the name.
Examples:
```go
r := rux.New()
// Method 1
myRoute := rux.NewNamedRoute("name1", "/path4/some/{id}", emptyHandler, "GET")
r.AddRoute(myRoute)
// Method 2
rux.AddNamed("name2", "/", func(c *rux.Context) {
c.Text(200, "hello")
})
// Method 3
r.GET("/hi", func(c *rux.Context) {
c.Text(200, "hello")
}).NamedTo("name3", r)
// get route by name
myRoute = r.GetRoute("name1")
```
### Redirect
redirect to other page
```go
r.GET("/", func(c *rux.Context) {
c.AbortThen().Redirect("/login", 302)
})
// Or
r.GET("/", func(c *rux.Context) {
c.Redirect("/login", 302)
c.Abort()
})
r.GET("/", func(c *rux.Context) {
c.Back()
c.Abort()
})
```
### Cookies
you can quick operate cookies by `FastSetCookie()` `DelCookie()`
> Note: You must set or delete cookies before writing BODY content
```go
r.GET("/setcookie", func(c *rux.Context) {
c.FastSetCookie("rux_cookie2", "test-value2", 3600)
c.SetCookie("rux_cookie", "test-value1", 3600, "/", c.Req.URL.Host, false, true)
c.WriteString("hello, in " + c.URL().Path)
})
// FastSetCookie accepts optional func(*http.Cookie) callbacks to override
// the developer-friendly defaults — handy for HTTPS / SameSite.
r.GET("/setsecure", func(c *rux.Context) {
c.FastSetCookie("session", "v", 3600, func(ck *http.Cookie) {
ck.Secure = true
ck.SameSite = http.SameSiteStrictMode
})
})
r.GET("/delcookie", func(c *rux.Context) {
val := ctx.Cookie("rux_cookie") // "test-value1"
c.DelCookie("rux_cookie", "rux_cookie2")
})
```
### Multi Domains
> code is refer from `julienschmidt/httprouter`
```go
package main
import (
"log"
"net/http"
"github.com/gookit/rux/v2"
)
type HostSwitch map[string]http.Handler
// Implement the ServeHTTP method on our new type
func (hs HostSwitch) ServeHTTP(w http.ResponseWriter, r *http.Request) {
// Check if a http.Handler is registered for the given host.
// If yes, use it to handle the request.
if router := hs[r.Host]; router != nil {
router.ServeHTTP(w, r)
} else {
// Handle host names for which no handler is registered
http.Error(w, "Forbidden", 403) // Or Redirect?
}
}
func main() {
// Initialize a router as usual
router := rux.New()
router.GET("/", Index)
router.GET("/hello/{name}", func(c *rux.Context) {})
// Make a new HostSwitch and insert the router (our http handler)
// for example.com and port 12345
hs := make(HostSwitch)
hs["example.com:12345"] = router
// Use the HostSwitch to listen and serve on port 12345
log.Fatal(http.ListenAndServe(":12345", hs))
}
```
### RESETFul Style
```go
package main
import (
"log"
"net/http"
"github.com/gookit/rux/v2"
)
type Product struct {
}
// Uses middlewares [optional]
func (Product) Uses() map[string][]rux.HandlerFunc {
return map[string][]rux.HandlerFunc{
// function name: handlers
"Delete": []rux.HandlerFunc{
handlers.HTTPBasicAuth(map[string]string{"test": "123"}),
handlers.GenRequestID(),
},
}
}
// all products [optional]
func (p *Product) Index(c *rux.Context) { }
// create product [optional]
func (p *Product) Create(c *rux.Context) { }
// save new product [optional]
func (p *Product) Store(c *rux.Context) { }
// show product with {id} [optional]
func (p *Product) Show(c *rux.Context) { }
// edit product [optional]
func (p *Product) Edit(c *rux.Context) { }
// save edited product [optional]
func (p *Product) Update(c *rux.Context) { }
// delete product [optional]
func (p *Product) Delete(c *rux.Context) { }
func main() {
router := rux.New()
// methods Path Action Route Name
// GET /product index product_index
// GET /product/create create product_create
// POST /product store product_store
// GET /product/{id} show product_show
// GET /product/{id}/edit edit product_edit
// PUT/PATCH /product/{id} update product_update
// DELETE /product/{id} delete product_delete
// resetful style
router.Resource("/", new(Product))
log.Fatal(http.ListenAndServe(":12345", router))
}
```
### Controller Style
```go
package main
import (
"log"
"net/http"
"github.com/gookit/rux/v2"
)
// News controller
type News struct {
}
func (n *News) AddRoutes(g *rux.Router) {
g.GET("/", n.Index)
g.POST("/", n.Create)
g.PUT("/", n.Edit)
}
func (n *News) Index(c *rux.Context) { }
func (n *News) Create(c *rux.Context) { }
func (n *News) Edit(c *rux.Context) { }
func main() {
router := rux.New()
// controller style
router.Controller("/news", new(News))
log.Fatal(http.ListenAndServe(":12345", router))
}
```
### Build URL
```go
package main
import (
"log"
"net/http"
"github.com/gookit/rux/v2"
)
func main() {
// Initialize a router as usual
router := rux.New()
router.GET(`/news/{category_id}/{new_id}/detail`, func(c *rux.Context) {
var u = make(url.Values)
u.Add("username", "admin")
u.Add("password", "12345")
b := rux.NewBuildRequestURL()
// b.Scheme("https")
// b.Host("www.mytest.com")
b.Queries(u)
b.Params(rux.M{"{category_id}": "100", "{new_id}": "20"})
// b.Path("/dev")
// println(b.Build().String())
println(c.Router().BuildRequestURL("new_detail", b).String())
// result: /news/100/20/detail?username=admin&password=12345
// get current route name
if c.MustGet(rux.CTXCurrentRouteName) == "new_detail" {
// post data etc....
}
}).NamedTo("new_detail", router)
// Use the HostSwitch to listen and serve on port 12345
log.Fatal(http.ListenAndServe(":12345", router))
}
```
## Request Binding and Validation
`pkg/binding` supports form, query, header, JSON and XML binding. Validation is an extension hook and is disabled by default, so rux does not pull in a validation library unless your application chooses one.
To integrate [gookit/validate](https://github.com/gookit/validate), install an adapter once at startup:
```go
package main
import (
"github.com/gookit/rux/v2/pkg/binding"
"github.com/gookit/validate"
)
type validateAdapter struct{}
func (validateAdapter) Validate(obj any) error {
v := validate.New(obj)
if v.Validate() {
return nil
}
return v.Errors.OneError()
}
func main() {
binding.Validator = validateAdapter{}
}
```
After the adapter is installed, `c.Bind`, `c.BindJSON`, `binding.Auto`, and other built-in binders validate the bound object automatically. See `_examples/validate` for a runnable example.
## Production-Ready Server
Package `server` wraps a `rux.Router` with sensible HTTP timeouts, graceful shutdown, lifecycle hooks,
and built-in `/healthz` / `/readyz` endpoints. It is the recommended way to run rux in containers / k8s.
```go
package main
import (
"context"
"log"
"github.com/gookit/rux/v2"
"github.com/gookit/rux/v2/server"
)
func main() {
s := server.New(false) // false = no debug logging
s.Addr = ":8080"
// Global middleware. Use works any time before the first request; declaring
// it up front is the clearest style.
s.Use(myAuthMiddleware)
// The routes, health checks included.
s.GET("/", func(c *rux.Context) {
c.Text(200, "hello")
})
// Optional liveness/readiness endpoints under /healthz and /readyz.
// The global chain covers them too.
s.MountHealthChecks()
// Optional lifecycle hooks (warm caches, validate config, etc.).
s.PreStart = append(s.PreStart, func(ctx context.Context) error {
return nil
})
if err := s.Run(); err != nil {
log.Fatal(err)
}
}
```
What `Run()` does for you:
- Bind the listener up front, then serve (`Serve`, or `ServeTLS` when
`TLSCertFile`/`TLSKeyFile` are set). Binding happens before the `PostStart`
hooks run, so hooks always see the resolved address
- Wait for `SIGINT` / `SIGTERM` (configurable via `StopSignals`)
- On signal: flip `/readyz` to 503 → wait `DrainDelay` so the upstream LB
can drain → call `http.Server.Shutdown` bounded by `ShutdownTimeout`
- Run `PreShutdown` / `PostShutdown` hooks in order
Defaults tuned for container deployments:
| Field | Default | Purpose |
| ------------------- | ------- | ------------------------------------ |
| `ReadHeaderTimeout` | 2s | slowloris defense |
| `ReadTimeout` | 10s | full request read budget |
| `WriteTimeout` | 30s | response write budget |
| `IdleTimeout` | 120s | keep-alive idle close |
| `DrainDelay` | 5s | LB drain window after stop signal |
| `ShutdownTimeout` | 25s | bound on graceful shutdown |
### Ephemeral ports and the bound address
When `Addr` asks for port 0 (`"127.0.0.1:0"` or `":0"`) the kernel picks the
port. The resolved address is exposed through `ListenAddr()`, `ListenPort()` and
`LocalURL()`, and `WaitListening(ctx)` tells you when it is final (it waits for
the real bind, not a timer) or returns the startup error when binding fails:
```go
s := server.New(false)
s.Addr = "127.0.0.1:0" // let the OS pick a free port
go func() { _ = s.Run() }()
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
if err := s.WaitListening(ctx); err != nil {
log.Fatal(err)
}
Frequently Asked Questions
What is rux?
rux is a Web Frameworks library for the Go programming language. Simple and fast web framework for build golang HTTP applications
How do I install rux?
Install rux with the Go module system using `go get gookit/rux`. Check the repository for the current installation instructions.
What category does rux belong to?
rux is listed under Web Frameworks, specifically Web Frameworks.