Skip to content

Latest commit

 

History

26 Commits

Folders and files

NameName
Last commit message
Last commit date
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

Restful Extended eXperience (REX)

A lightweight, extension-based HTTP routing framework for Go with lifecycle management and event-driven architecture.

Go Version Coverage License

Overview

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 loggingslog-backed default logger with structured fields

Installation

go get github.com/kryovyx/rex

Extension Ecosystem

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

Quick Start

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)
    }
}

Core Concepts

Rex Interface

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
}

Routing

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")

Route Context

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)
}

Event System

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

Dependency Injection

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

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)

Multi-Router

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.

Declaration order does not matter

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.

Configuration

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.

Logging

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
}

Complete Example

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)
    }
}

Architecture

+-------------------------------------------------------------+
|                           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

Upgrading

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.

Contributing

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.

License

This project is licensed under the MIT License — see the LICENSE file for details.

Copyright

© 2026 Kryovyx

About

Restful Extended eXperience — a lightweight, extension-based HTTP routing framework for Go with lifecycle management, dependency injection and an event-driven architecture.

Topics

Resources

Contributing

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Used by

Contributors

Languages