A lightweight, extension-based HTTP routing framework for Go with lifecycle management and event-driven architecture.
rex is a modular HTTP framework for Go built around extensions and lifecycle hooks:
- Trie-based routing — Fast path matching with
{param}path parameters - Extension system — Five lifecycle hooks (
OnInitialize,OnStart,OnReady,OnStop,OnShutdown) - Event-driven architecture — Typed events for routing and request lifecycle
- Dependency injection — Built-in integration with dix
- Multi-router support — Run multiple listeners on different addresses
- Graceful shutdown — Clean stop with configurable timeout
- Pluggable logging —
slog-backed default logger with structured fields
go get github.com/kryovyx/rex
| Module | Import | Description |
|---|---|---|
| rextension | github.com/kryovyx/rextension |
Minimal interface contracts for extension authors |
| rextension-cors | github.com/kryovyx/rextension-cors |
Cross-Origin Resource Sharing |
| rextension-health | github.com/kryovyx/rextension-health |
Health, readiness & liveness checks |
| rextension-metric | github.com/kryovyx/rextension-metric |
Prometheus / OpenMetrics instrumentation |
| rextension-openapi | github.com/kryovyx/rextension-openapi |
OpenAPI 3.1 spec generation |
| rextension-ratelimit | github.com/kryovyx/rextension-ratelimit |
Global, per-router and per-endpoint rate limiting |
| rextension-security | github.com/kryovyx/rextension-security |
Authentication middleware |
| rextension-swagger | github.com/kryovyx/rextension-swagger |
Swagger UI serving |
| rextension-validation | github.com/kryovyx/rextension-validation |
Request / response validation |
package main import ( "github.com/kryovyx/rex" "github.com/kryovyx/rex/route" ) func main() { app := rex.New() app.WithOptions( rex.WithConfig(&rex.Config{ DefaultRouter: rex.RouterConfig{Addr: ":8080"}, }), ) app.RegisterRoute(route.New("GET", "/hello", func(ctx route.Context) { ctx.Text(200, "Hello, World!") })) if err := app.Run(); err != nil { panic(err) } }
The top-level entry point combines framework control with the extension-facing surface:
type Rex interface { rextension.Rex // Logger, Container, EventBus, routing, middleware WithOptions(options ...Option) Rex // Apply functional options WithExtensions(ext ...Extension) rextension.Rex // Register extensions WithLogger(l logger.Logger) Rex // Set custom logger Run() error // Start the application Stop() error // Graceful shutdown Config() *Config // Read current config }
The embedded rextension.Rex interface exposes what extensions need:
type Rex interface { Logger() Logger Container() dix.Container EventBus() EventBus Use(mw Middleware) RegisterRoute(rt Route) error RegisterRouteToRouter(rt Route, routerName string) error CreateRouter(name string, cfg RouterConfig) error }
Routes are created with route.New(method, path, handler) and registered on the app or a named router.
// Basic routes app.RegisterRoute(route.New("GET", "/users", listUsers)) app.RegisterRoute(route.New("POST", "/users", createUser)) app.RegisterRoute(route.New("GET", "/users/{id}", getUser)) // Named router app.CreateRouter("admin", rex.RouterConfig{Addr: ":9090"}) app.RegisterRouteToRouter(route.New("GET", "/dashboard", adminDash), "admin")
Every handler receives a route.Context:
type Context interface { context.Context ResponseWriter() http.ResponseWriter Request() *http.Request Resolver() dix.Resolver // Request-scoped DI Param(name string) string // Captured path parameter, e.g. Param("id") Respond(status int, contentType string, body interface{}) error Text(status int, v string) error JSON(status int, v interface{}) error OpenMetrics(status int, v interface{}) error SetValue(key, value interface{}) GetValue(key interface{}) interface{} }
func getUser(ctx route.Context) { var svc *UserService ctx.Resolver().Resolve(&svc) user := svc.FindByID(ctx.Param("id")) ctx.JSON(200, user) }
The event bus lets extensions and application code react to framework events:
app.EventBus().Subscribe("router.route.registered", func(e event.Event) { app.Logger().Info("route registered") }) app.EventBus().Subscribe("router.request.incoming", func(e event.Event) { app.Logger().Debug("incoming request") })
Built-in event types:
| Event Type | Fired When |
|---|---|
router.initialized |
Router trie is built |
router.route.registered |
A route is added |
router.request.incoming |
An HTTP request arrives |
router.request.handled |
A request finishes |
router.request.unresolved |
No matching route found |
Rex wraps dix for DI. Services registered in the container are available in handlers via ctx.Resolver():
app.Container().Singleton(func() *Database { return &Database{DSN: "postgres://localhost/mydb"} }) // A factory may take a dix.Resolver. For a Scoped registration it receives // the *scope*, so any scoped dependency it resolves stays inside the same // scope — closing over the container instead would resolve from the root, // which returns dix.ErrScopedFromRoot. app.Container().Scoped(func(r dix.Resolver) *UserRepo { var db *Database if err := r.Resolve(&db); err != nil { panic(err) } return &UserRepo{DB: db} }) // Inside a handler func listUsers(ctx route.Context) { var repo *UserRepo ctx.Resolver().Resolve(&repo) ctx.JSON(200, repo.FindAll()) }
Middleware uses the standard Go signature func(http.Handler) http.Handler:
func LoggingMiddleware(next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { start := time.Now() next.ServeHTTP(w, r) log.Printf("%s %s %v", r.Method, r.URL.Path, time.Since(start)) }) } app.Use(LoggingMiddleware)
Run multiple HTTP listeners (e.g. public API + admin panel):
app := rex.New( rex.WithConfig(&rex.Config{ DefaultRouter: rex.RouterConfig{Addr: ":8080"}, }), ) if err := app.CreateRouter("admin", rex.RouterConfig{Addr: ":9090"}); err != nil { return err } // default :8080 if err := app.RegisterRoute(route.New("GET", "/api/users", listUsers)); err != nil { return err } // admin :9090 if err := app.RegisterRouteToRouter(route.New("GET", "/dashboard", adminDash), "admin"); err != nil { return err }
Registration returns an error. It is worth checking: a route with a typo'd path, a missing handler or a duplicate is rejected there, and discarding the return value means the route silently never appears.
New() collects; Run() builds. Configuration, routers, routes and middleware
may be declared in any order before Run, and are resolved together when the
route tables are built:
app := rex.New() // Middleware before the router it applies to — fine. app.Use(loggingMiddleware) _ = app.CreateRouter("admin", rex.RouterConfig{Addr: ":9090"}) // A route before its router — also fine. _ = app.RegisterRouteToRouter(route.New("GET", "/x", h), "metrics") _ = app.CreateRouter("metrics", rex.RouterConfig{Addr: ":9091"}) // Configuration after everything else — still fine. app.WithOptions(rex.WithConfig(cfg)) if err := app.Run(); err != nil { // everything is resolved here return err }
What cannot be known until Run is reported from Run — an unknown router
name, for instance, wrapped with the registration that named it. Anything
checkable at the call site is returned there.
type Config struct { DefaultRouter RouterConfig ShutdownTimeout time.Duration // default: 10s } type RouterConfig struct { Addr string // default: ":8080" BaseURL string // default: "/" ListenSSL bool // default: false — TLS is opt-in CertFile *string // path to TLS cert; nil = no TLS KeyFile *string // path to TLS key; nil = no TLS TLSConfig *tls.Config // takes precedence; also where mTLS is configured // Listener limits. Zero takes the default; negative disables. ReadHeaderTimeout time.Duration // default 10s — the Slowloris bound ReadTimeout time.Duration // default 30s WriteTimeout time.Duration // default 0 — unset, deliberately IdleTimeout time.Duration // default 120s MaxHeaderBytes int // default 1 MiB MaxBodyBytes int64 // default 4 MiB }
WriteTimeout stays 0 on purpose: it is an absolute deadline on the whole response, so
a non-zero value truncates SSE, long polling and large downloads. Slowloris is a read
attack, closed by ReadHeaderTimeout.
SSLVerify was removed — it configured nothing. Client certificate verification is
TLSConfig.ClientAuth plus TLSConfig.ClientCAs.
Apply configuration via options:
app.WithOptions(rex.WithConfig(&rex.Config{ DefaultRouter: rex.RouterConfig{ Addr: ":443", BaseURL: "/api", CertFile: ptr("cert.pem"), KeyFile: ptr("key.pem"), }, ShutdownTimeout: 30 * time.Second, }))
ListenSSL defaults to true. When both CertFile and KeyFile are provided the router binds with TLS. Set ListenSSL: false to force plain HTTP even with certificates present.
Rex ships with SlogLogger (backed by log/slog). No external dependencies required.
import "github.com/kryovyx/rex/logger" // Use default app := rex.New() // Or set a custom level app.WithLogger(logger.NewSlogLoggerWithLevel(logger.LogLevelDebug))
The logger interface:
type Logger interface { Info(format string, args ...interface{}) Warn(format string, args ...interface{}) Error(format string, args ...interface{}) Debug(format string, args ...interface{}) Trace(format string, args ...interface{}) SetLogLevel(level LogLevel) WithField(key string, value interface{}) Logger WithFields(fields map[string]interface{}) Logger WithError(err error) Logger }
package main import ( "time" "github.com/kryovyx/rex" "github.com/kryovyx/rex/logger" "github.com/kryovyx/rex/route" ) type UserService struct{} func (s *UserService) Greet(name string) string { return "Hello, " + name } func main() { app := rex.New() app.WithLogger(logger.NewSlogLoggerWithLevel(logger.LogLevelDebug)) app.WithOptions(rex.WithConfig(&rex.Config{ DefaultRouter: rex.RouterConfig{Addr: ":8080"}, ShutdownTimeout: 30 * time.Second, })) // DI app.Container().Singleton(func() *UserService { return &UserService{} }) // Routes app.RegisterRoute(route.New("GET", "/", func(ctx route.Context) { ctx.JSON(200, map[string]string{"message": "Welcome to Rex!"}) })) app.RegisterRoute(route.New("GET", "/greet/{name}", func(ctx route.Context) { var svc *UserService ctx.Resolver().Resolve(&svc) ctx.Text(200, svc.Greet(ctx.Param("name"))) })) if err := app.Run(); err != nil { panic(err) } }
+-------------------------------------------------------------+
| Rex |
+-------------------------------------------------------------+
| Extensions Logger DI Container (dix) |
+-------------------------------------------------------------+
| Event Bus |
+-------------------------------------------------------------+
| Router 1 (default) Router 2 (admin) Router N ... |
| :8080 :9090 :<port> |
| Trie Routes Trie Routes Trie Routes |
+-------------------------------------------------------------+
Lifecycle:
OnInitialize -> OnStart -> [listeners bind] -> OnReady
... serving ...
OnStop -> [listeners close] -> OnShutdown
v0.2.1 → v0.3.0. MIGRATION.md is the upgrade guide for this
module, written to stand alone — it carries the dependency-ordered go get
sequence, every breaking change here, and what to verify afterwards. Other
modules of the framework each have their own; that file links to them.
The framework is in alpha, and external contributions open at v1.0.0.
Until then pull requests will be closed unmerged — but issues are very welcome.
Bug reports, questions and feature requests all feed into what v1.0.0 looks
like.
See CONTRIBUTING.md for the rules that will apply, and COMMIT-CONVENTIONS.md for the commit format.
This project is licensed under the MIT License — see the LICENSE file for details.
© 2026 Kryovyx