Files
httpx/server/route.go
Aleksey Shakhmatov cea75d198b Add production-ready HTTP server package with routing, health checks, and middleware
Introduces server/ sub-package as the server-side companion to the existing Client.
Includes Router (over http.ServeMux with groups and mounting), graceful shutdown with
signal handling, health endpoints (/healthz, /readyz), and built-in middlewares
(RequestID, Recovery, Logging). Zero external dependencies.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-21 13:41:54 +03:00

104 lines
3.0 KiB
Go

package server
import (
"net/http"
"strings"
)
// Router is a lightweight wrapper around http.ServeMux that adds middleware
// groups and sub-router mounting. It leverages Go 1.22+ enhanced patterns
// like "GET /users/{id}".
type Router struct {
mux *http.ServeMux
prefix string
middlewares []Middleware
}
// NewRouter creates a new Router backed by a fresh http.ServeMux.
func NewRouter() *Router {
return &Router{
mux: http.NewServeMux(),
}
}
// Handle registers a handler for the given pattern. The pattern follows
// http.ServeMux conventions, including method-based patterns like "GET /users".
func (r *Router) Handle(pattern string, handler http.Handler) {
if len(r.middlewares) > 0 {
handler = Chain(r.middlewares...)(handler)
}
r.mux.Handle(r.prefixedPattern(pattern), handler)
}
// HandleFunc registers a handler function for the given pattern.
func (r *Router) HandleFunc(pattern string, fn http.HandlerFunc) {
r.Handle(pattern, fn)
}
// Group creates a sub-router with a shared prefix and optional middleware.
// Patterns registered on the group are prefixed automatically. The group
// shares the underlying ServeMux with the parent router.
//
// Example:
//
// api := router.Group("/api/v1", authMiddleware)
// api.HandleFunc("GET /users", listUsers) // registers "GET /api/v1/users"
func (r *Router) Group(prefix string, mws ...Middleware) *Router {
return &Router{
mux: r.mux,
prefix: r.prefix + prefix,
middlewares: append(r.middlewaresSnapshot(), mws...),
}
}
// Mount attaches an http.Handler under the given prefix. All requests
// starting with prefix are forwarded to the handler with the prefix stripped.
func (r *Router) Mount(prefix string, handler http.Handler) {
full := r.prefix + prefix
if !strings.HasSuffix(full, "/") {
full += "/"
}
r.mux.Handle(full, http.StripPrefix(strings.TrimSuffix(full, "/"), handler))
}
// ServeHTTP implements http.Handler, making Router usable as a handler.
func (r *Router) ServeHTTP(w http.ResponseWriter, req *http.Request) {
r.mux.ServeHTTP(w, req)
}
// prefixedPattern inserts the router prefix into a pattern. It is aware of
// method prefixes: "GET /users" with prefix "/api" becomes "GET /api/users".
func (r *Router) prefixedPattern(pattern string) string {
if r.prefix == "" {
return pattern
}
// Split method prefix if present: "GET /users" → method="GET ", path="/users"
method, path, hasMethod := splitMethodPattern(pattern)
path = r.prefix + path
if hasMethod {
return method + path
}
return path
}
// splitMethodPattern splits "GET /path" into ("GET ", "/path", true).
// If there is no method prefix, returns ("", pattern, false).
func splitMethodPattern(pattern string) (method, path string, hasMethod bool) {
if idx := strings.IndexByte(pattern, ' '); idx >= 0 {
return pattern[:idx+1], pattern[idx+1:], true
}
return "", pattern, false
}
func (r *Router) middlewaresSnapshot() []Middleware {
if len(r.middlewares) == 0 {
return nil
}
cp := make([]Middleware, len(r.middlewares))
copy(cp, r.middlewares)
return cp
}