| name | rweb-light-go-webserver |
| description | Build HTTP web servers with the light and low-dependency RWeb Go framework. Covers routing, middleware, cookies, groups, SSE, SSE Hub (multi-client broadcast), WebSockets, static files, proxying, and file uploads. |
RWeb Framework Skill
RWeb is a high-performance, lightweight HTTP web server framework for Go featuring a custom radix tree router and practically zero third party dependencies.
Getting Started
Basic Server Setup
package main
import (
"log"
"github.com/rohanthewiz/rweb"
)
func main() {
s := rweb.NewServer(rweb.ServerOptions{
Address: ":8080",
Verbose: true,
Debug: false,
})
s.Get("/", func(ctx rweb.Context) error {
return ctx.WriteString("Hello, World!")
})
log.Fatal(s.Run())
}
TLS/HTTPS Configuration
Note: the TLS listener binds TLS.TLSAddr (not Address). Address is the plain-HTTP address, used by RunWithHttpsRedirect() for the 80 -> 443 redirect server.
s := rweb.NewServer(rweb.ServerOptions{
Address: ":80",
TLS: rweb.TLSCfg{
UseTLS: true,
TLSAddr: ":443",
KeyFile: "certs/localhost.key",
CertFile: "certs/localhost.crt",
},
})
For dynamic certificates — Let's Encrypt autocert or hot-reload of renewed cert files — supply TLS.Config instead of Cert/KeyFile; it is used as the listener's *tls.Config (MinVersion defaults to TLS 1.2 if unset):
m := &autocert.Manager{
Prompt: autocert.AcceptTOS,
HostPolicy: autocert.HostWhitelist("example.org"),
Cache: autocert.DirCache("certs/autocert"),
}
s := rweb.NewServer(rweb.ServerOptions{
Address: ":80",
TLS: rweb.TLSCfg{
UseTLS: true,
TLSAddr: ":443",
Config: m.TLSConfig(),
},
})
go http.ListenAndServe(":80", m.HTTPHandler(nil))
log.Fatal(s.Run())
Routing
HTTP Methods
s.Get("/path", handler)
s.Post("/path", handler)
s.Put("/path", handler)
s.Delete("/path", handler)
Route Parameters
s.Get("/users/:id", func(ctx rweb.Context) error {
id := ctx.Request().PathParam("id")
return ctx.WriteString("User ID: " + id)
})
s.Get("/orgs/:org/repos/:repo", func(ctx rweb.Context) error {
org := ctx.Request().PathParam("org")
repo := ctx.Request().PathParam("repo")
return ctx.WriteJSON(map[string]string{"org": org, "repo": repo})
})
Fixed vs Parameterized Routes
Fixed routes take precedence over parameterized ones when there's an exact match:
s.Get("/greet/:name", handler)
s.Get("/greet/city", handler)
Response Types
ctx.WriteString("Hello")
ctx.WriteHTML("<h1>Welcome</h1>")
ctx.WriteJSON(map[string]string{"status": "ok"})
rweb.CSS(ctx, "body { color: red; }")
rweb.File(ctx, "filename.txt", fileBytes)
ctx.SetStatus(404).WriteString("Not found")
ctx.Redirect(302, "/new-location")
Middleware
Middleware executes in registration order. Call ctx.Next() to continue the chain.
s.Use(func(ctx rweb.Context) error {
start := time.Now()
defer func() {
fmt.Printf("%s %s -> %d [%s]\n",
ctx.Request().Method(),
ctx.Request().Path(),
ctx.Response().Status(),
time.Since(start))
}()
return ctx.Next()
})
s.Use(rweb.RequestInfo)
Context Data Storage
Store request-scoped data accessible to all middleware and handlers:
ctx.Set("userId", "123")
ctx.Set("isAdmin", true)
userId := ctx.Get("userId").(string)
if ctx.Has("isLoggedIn") {
}
ctx.Delete("userId")
Authentication Pattern
s.Use(func(ctx rweb.Context) error {
authHeader := ctx.Request().Header("Authorization")
if authHeader == "Bearer valid-token" {
ctx.Set("isLoggedIn", true)
ctx.Set("userId", "123")
}
return ctx.Next()
})
s.Get("/profile", func(ctx rweb.Context) error {
if !ctx.Has("isLoggedIn") || !ctx.Get("isLoggedIn").(bool) {
return ctx.SetStatus(401).WriteString("Unauthorized")
}
return ctx.WriteJSON(map[string]string{"userId": ctx.Get("userId").(string)})
})
Route Groups
Organize routes with common prefixes and middleware:
api := s.Group("/api")
v1 := api.Group("/v1")
v1.Get("/status", statusHandler)
users := v1.Group("/users", authMiddleware)
users.Get("/", listUsers)
users.Get("/:id", getUser)
users.Post("/", createUser)
admin := s.Group("/admin", authMiddleware, adminMiddleware)
admin.Get("/dashboard", dashboardHandler)
Cookies
Server-wide Cookie Config
s := rweb.NewServer(rweb.ServerOptions{
Address: ":8080",
Cookie: rweb.CookieConfig{
HttpOnly: true,
SameSite: rweb.SameSiteLaxMode,
Path: "/",
},
})
Cookie Operations
ctx.SetCookie("name", "value")
cookie := &rweb.Cookie{
Name: "session_id",
Value: sessionID,
Expires: time.Now().Add(30 * 24 * time.Hour),
MaxAge: 30 * 24 * 60 * 60,
}
ctx.SetCookieWithOptions(cookie)
value, err := ctx.GetCookie("name")
if ctx.HasCookie("session_id") {
}
ctx.DeleteCookie("name")
value, err := ctx.GetCookieAndClear("flash")
Static Files
s.StaticFiles("/static/images/", "./assets/images", 2)
s.StaticFiles("/css/", "./assets/css", 1)
s.StaticFiles("/.well-known/", "./", 0)
File Uploads
s.Post("/upload", func(ctx rweb.Context) error {
req := ctx.Request()
name := req.FormValue("name")
file, header, err := req.GetFormFile("file")
if err != nil {
return err
}
defer file.Close()
data, err := io.ReadAll(file)
if err != nil {
return err
}
return os.WriteFile("uploads/"+header.Filename, data, 0666)
})
Server-Sent Events (SSE)
Single-Channel SSE
For simple cases where one channel feeds one endpoint:
eventsChan := make(chan any, 100)
s.Get("/events", func(ctx rweb.Context) error {
return s.SetupSSE(ctx, eventsChan)
})
s.Get("/events2", s.SSEHandler(eventsChan))
eventsChan <- "event data"
eventsChan <- map[string]string{"type": "update", "data": "value"}
SSE Hub (Multi-Client Broadcast)
SSEHub provides a fan-out pattern for broadcasting events to all connected clients.
Each client gets its own buffered channel, and the hub manages registration and
cleanup automatically. The hub is standalone — not tied to a specific server or route.
Basic Usage (zero-config)
hub := rweb.NewSSEHub()
s.Get("/logs/stream", hub.Handler(s))
hub.Broadcast(rweb.SSEvent{
Type: "log",
Data: "2026-02-21 10:05:32 [INFO] User logged in",
})
hub.BroadcastAny("error", "disk usage at 92%")
hub.BroadcastRaw(rweb.SSEvent{Type: "heartbeat", Data: "ok"})
count := hub.ClientCount()
SSEHubOptions (Hardened Configuration)
For production environments with frequent disconnections, proxies, or load balancers,
pass SSEHubOptions to configure heartbeat keepalives and stale client eviction:
hub := rweb.NewSSEHub(rweb.SSEHubOptions{
ChannelSize: 16,
MaxDropped: 3,
HeartbeatInterval: 30 * time.Second,
OnDisconnect: func() {
fmt.Println("Client disconnected")
},
})
defer hub.Close()
Log Stream Example
A real-time log viewer that tails application logs to all connected browsers:
func main() {
s := rweb.NewServer(
rweb.WithAddress(":8080"),
rweb.WithVerbose(),
)
logHub := rweb.NewSSEHub(rweb.SSEHubOptions{
HeartbeatInterval: 30 * time.Second,
})
defer logHub.Close()
s.Get("/logs/stream", logHub.Handler(s))
s.Get("/logs/viewers", func(ctx rweb.Context) error {
return ctx.WriteJSON(map[string]any{
"viewers": logHub.ClientCount(),
})
})
go func() {
entries := []string{
"[INFO] Server started on :8080",
"[INFO] Connected to database",
"[WARN] Slow query detected (1.2s)",
"[ERROR] Failed to send email: timeout",
"[INFO] User alice logged in",
}
i := 0
for range time.NewTicker(2 * time.Second).C {
logHub.Broadcast(rweb.SSEvent{
Type: "log",
Data: fmt.Sprintf("%s %s", time.Now().Format("15:04:05"), entries[i%len(entries)]),
})
i++
}
}()
log.Fatal(s.Run())
}
JS Client for SSE Hub
Since Broadcast() sends JSON-wrapped data under the standard "message" event type,
JS clients only need onmessage — no addEventListener required:
const evtSource = new EventSource('/logs/stream');
evtSource.onmessage = function(e) {
const payload = JSON.parse(e.data);
console.log('[' + payload.type + ']', payload.data);
};
evtSource.onerror = function() {
console.log('Disconnected — EventSource will auto-reconnect');
};
WebSockets
Registration
Use s.WebSocket() to register a handler that receives an upgraded *rweb.WSConn.
The framework handles the HTTP upgrade handshake automatically.
s.WebSocket("/ws/echo", func(ws *rweb.WSConn) error {
defer ws.Close(1000, "Closing")
fmt.Printf("Client connected from %s\n", ws.RemoteAddr())
ws.WriteMessage(rweb.TextMessage, []byte("Welcome"))
for {
msg, err := ws.ReadMessage()
if err != nil {
break
}
switch msg.Type {
case rweb.TextMessage:
ws.WriteMessage(rweb.TextMessage, msg.Data)
case rweb.BinaryMessage:
ws.WriteMessage(rweb.BinaryMessage, msg.Data)
case rweb.CloseMessage:
return nil
}
}
return nil
})
Message Types
rweb.TextMessage
rweb.BinaryMessage
rweb.CloseMessage
rweb.PingMessage
rweb.PongMessage
WSConn API Reference
msg, err := ws.ReadMessage()
ws.WriteMessage(rweb.TextMessage, data)
ws.Close(1000, "reason")
ws.OnClose(func(code int, text string) {
fmt.Printf("Closed: %d %s\n", code, text)
})
ws.WritePing([]byte("ping"))
ws.SetPongHandler(func(data []byte) error {
return nil
})
ws.SetPingHandler(func(data []byte) error {
return nil
})
<-ws.Done()
ws.SetMaxMessageSize(10 * 1024 * 1024)
ws.SetReadDeadline(time.Now().Add(60 * time.Second))
ws.SetWriteDeadline(time.Now().Add(10 * time.Second))
ws.RemoteAddr()
ws.LocalAddr()
Chat Server with Broadcasting
A multi-client chat pattern using a Hub to manage connections:
type Hub struct {
clients map[*rweb.WSConn]bool
broadcast chan []byte
register chan *rweb.WSConn
unregister chan *rweb.WSConn
mu sync.RWMutex
}
func (h *Hub) Run() {
for {
select {
case client := <-h.register:
h.mu.Lock()
h.clients[client] = true
h.mu.Unlock()
case client := <-h.unregister:
h.mu.Lock()
if _, ok := h.clients[client]; ok {
delete(h.clients, client)
client.Close(1000, "Disconnected")
}
h.mu.Unlock()
case message := <-h.broadcast:
h.mu.RLock()
for client := range h.clients {
if err := client.WriteMessage(rweb.TextMessage, message); err != nil {
go func(c *rweb.WSConn) { h.unregister <- c }(client)
}
}
h.mu.RUnlock()
}
}
}
s.WebSocket("/ws/chat", func(ws *rweb.WSConn) error {
hub.register <- ws
defer func() { hub.unregister <- ws }()
go func() {
ticker := time.NewTicker(20 * time.Second)
defer ticker.Stop()
for {
select {
case <-ws.Done():
return
case <-ticker.C:
ws.WritePing([]byte("ping"))
}
}
}()
for {
msg, err := ws.ReadMessage()
if err != nil {
break
}
if msg.Type == rweb.TextMessage {
hub.broadcast <- msg.Data
}
}
return nil
})
Manual Upgrade
For advanced use cases, you can upgrade the connection manually in a regular handler
instead of using s.WebSocket():
s.Get("/ws/custom", func(ctx rweb.Context) error {
if !ctx.IsWebSocketUpgrade() {
return ctx.SetStatus(400).WriteString("Expected WebSocket upgrade")
}
ws, err := ctx.UpgradeWebSocket()
if err != nil {
return err
}
defer ws.Close(1000, "Done")
return nil
})
Reverse Proxy
err := s.Proxy("/api/backend", "http://backend:8081", 2)
if err != nil {
log.Fatal(err)
}
Request Information
req := ctx.Request()
req.Method()
req.Path()
req.PathParam("id")
req.Header("Authorization")
req.Body()
req.FormValue("field")
req.GetPostValue("field")
Handler Signature
All handlers follow this signature:
func(ctx rweb.Context) error
Return nil for success, or an error which rweb will handle appropriately.
Testing Endpoints
curl http://localhost:8080/
curl -H "Authorization: Bearer token" http://localhost:8080/api/users
curl -X POST -d "name=John" http://localhost:8080/users
curl -X POST -F "file=@document.pdf" http://localhost:8080/upload
curl -X POST -H "Content-Type: application/json" -d '{"name":"John"}' http://localhost:8080/users