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>
This commit is contained in:
103
server/route.go
Normal file
103
server/route.go
Normal file
@@ -0,0 +1,103 @@
|
||||
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
|
||||
}
|
||||
Reference in New Issue
Block a user