fursy
HTTP router with type-safe generic handlers, automatic OpenAPI 3.1 generation from code, and RFC 9457 error responses
About fursy
This large README is shown as a plain-text preview. Read the full README at the source
# 🔥 FURSY
> **F**ast **U**niversal **R**outing **Sy**stem
Next-generation HTTP router for Go 1.27+ with blazing performance, type-safe generic methods, and minimal dependencies.
[](https://pkg.go.dev/github.com/coregx/fursy)
[](https://goreportcard.com/report/github.com/coregx/fursy)
[](https://github.com/coregx/fursy/actions)
[](https://codecov.io/gh/coregx/fursy)
[](https://opensource.org/licenses/MIT)
[](https://github.com/coregx/fursy/releases)
---
## ⚡ Quick Start
```go
package main
import (
"log"
"net/http"
"github.com/coregx/fursy"
)
func main() {
router := fursy.New()
// Optional: Set validator for automatic validation
// router.SetValidator(validator.New())
// Simple text response — plain handler via Handle()
router.Handle("GET", "/", func(c *fursy.Context) error {
return c.Text("Welcome to FURSY!") // 200 OK
})
// GET with convenience method (200 OK) — plain handler via Handle()
router.Handle("GET", "/users/:id", func(c *fursy.Context) error {
id := c.Param("id")
return c.OK(map[string]string{
"id": id,
"name": "User " + id,
})
})
// POST with type-safe generic method (Go 1.27+)
router.POST("/users", func(c *fursy.Box[CreateUserRequest, UserResponse]) error {
// c.ReqBody is automatically bound and validated!
user := createUser(c.ReqBody)
return c.Created("/users/"+user.ID, user)
})
// DELETE with type-safe generic method
router.DELETE("/users/:id", func(c *fursy.Box[DeleteRequest, fursy.Empty]) error {
deleteUser(c.Param("id"))
return c.NoContentSuccess() // 204 No Content
})
// Query parameters — plain handler via Handle()
router.Handle("GET", "/search", func(c *fursy.Context) error {
query := c.Query("q")
page := c.QueryDefault("page", "1")
return c.OK(map[string]string{
"query": query,
"page": page,
})
})
log.Println("Server starting on :8080...")
log.Fatal(http.ListenAndServe(":8080", router))
}
```
> **Note**: Plain handlers use `router.Handle("METHOD", path, handler)` with `*Context`. Type-safe generic methods like `router.POST()` use `Box[Req, Res]` with Go 1.27 type inference -- see [Type-Safe Handlers](#type-safe-handlers-first-in-go) below!
---
## 🌟 Why FURSY?
### Type-Safe Generic Methods (First in Go! Requires Go 1.27+)
Go 1.27 enables generic methods on concrete types. FURSY is the first router to use this:
```go
// Type parameters are inferred from the handler signature — no explicit [Req,Res] needed!
router.POST("/users", func(box *fursy.Box[CreateUserRequest, UserResponse]) error {
// Compile-time type safety
// Automatic validation
// Zero boilerplate
})
```
### Native RFC 9457 Problem Details
```json
{
"type": "https://fursy.coregx.dev/problems/validation-error",
"title": "Validation Failed",
"status": 400,
"errors": [...]
}
```
### Built-in OpenAPI 3.1 Generation
Request/response schemas are inferred from type-safe handlers (`Box[Req, Res]`);
add summaries, tags, and status codes via `RouteOptions`.
```go
router := fursy.New()
router.WithInfo(fursy.Info{
Title: "My API",
Version: "1.0.0",
})
router.POST("/users", createUser, &fursy.RouteOptions{
Summary: "Create user",
Tags: []string{"users"},
SuccessStatus: 201, // 204 emits no body
})
// Serve the generated OpenAPI 3.1 document at GET /openapi.json.
router.ServeOpenAPI("/openapi.json")
```
### Minimal Dependencies
- **Core Routing**: Zero external dependencies (stdlib only)
- **Middleware**: Minimal deps (JWT: golang-jwt/jwt, RateLimit: x/time)
- **Plugins**: Optional extensions (OpenTelemetry, validators)
- Predictable, minimal security surface
### Production-Ready Performance
- **~53 ns/op** static routes, **~75 ns/op** parametric routes
- **0 allocations/op** (zero-allocation routing)
- Zero-allocation radix tree routing
- Efficient context pooling
---
## 📦 Installation
```bash
go get github.com/coregx/fursy
```
**Requirements**: Go 1.27+ (uses generic methods introduced in Go 1.27)
---
## 🚀 Features
- ✅ **High Performance Routing** - ~53 ns/op static, ~75 ns/op parametric, 0 alloc/op
- ✅ **Type-Safe Generic Methods** - `router.POST()` with Box[Req, Res] and Go 1.27 type inference
- ✅ **Automatic Validation** - Set once, validate everywhere with 100+ tags
- ✅ **Content Negotiation** - RFC 9110 compliant, AI agent support
- ✅ **RFC 9457 Problem Details** - Standardized error responses
- ✅ **Minimal Dependencies** - Core routing: stdlib only, middleware: minimal deps
- ✅ **Middleware Pipeline** - Next/Abort pattern, pre-allocated buffers
- ✅ **Route Groups** - Nested groups with middleware inheritance
- ✅ **JWT Authentication** - Token validation, claims extraction
- ✅ **Rate Limiting** - Token bucket algorithm, per-IP/per-user
- ✅ **Security Headers** - OWASP 2025 compliant (CSP, HSTS, etc.)
- ✅ **Circuit Breaker** - Failure threshold, auto-recovery
- ✅ **Trailing Slash Handling** - Strip or redirect, bidirectional, HTTP-correct (301/308)
- ✅ **Graceful Shutdown** - Connection draining, Kubernetes-ready
- ✅ **Context Pooling** - Memory-efficient, prevents leaks
- ✅ **Convenience Methods** - REST-friendly shortcuts (OK, Created, NoContentSuccess)
- ✅ **Real-Time Communications** - SSE + WebSocket via stream library
- ✅ **Database Integration** - dbcontext pattern with transaction support
- ✅ **Production Boilerplate** - Complete DDD example with real-time features
---
## 🎛️ Middleware
FURSY includes **8 production-ready middleware** with minimal dependencies. Core middleware have **zero external dependencies** (stdlib only), with only 2 exceptions: JWT (golang-jwt/jwt) and RateLimit (x/time).
### Core Middleware (Zero Dependencies)
#### Logger
Structured logging with `log/slog` for comprehensive request tracking.
```go
import (
"log/slog"
"github.com/coregx/fursy/middleware"
)
// Default configuration
router.Use(middleware.Logger())
// With configuration
logger := slog.New(slog.NewJSONHandler(os.Stdout, nil))
router.Use(middleware.LoggerWithConfig(middleware.LoggerConfig{
Logger: logger,
SkipPaths: []string{"/health", "/metrics"},
}))
```
**Features**:
- ✅ Structured logging with `log/slog` (stdlib)
- ✅ Request method, path, status, latency, bytes written
- ✅ Client IP extraction (X-Real-IP, X-Forwarded-For)
- ✅ Skip paths or custom skip function
- ✅ JSON or text format support
- ✅ Zero external dependencies
---
#### Recovery
Panic recovery with stack traces and RFC 9457 Problem Details.
```go
router.Use(middleware.Recovery())
// With stack traces (development)
router.Use(middleware.RecoveryWithConfig(middleware.RecoveryConfig{
IncludeStackTrace: true,
}))
```
**Features**:
- ✅ Automatic panic recovery
- ✅ Stack trace logging
- ✅ RFC 9457 error responses
- ✅ Custom error handler
- ✅ Production-safe (no stack traces by default)
- ✅ Zero external dependencies
---
#### CORS
Cross-Origin Resource Sharing (RFC-compliant, OWASP recommended).
> **Note**: CORS middleware **must** be registered globally via `router.Use()` **and before any auth middleware** (JWT, BasicAuth). Preflight OPTIONS requests don't carry credentials — if auth runs first, preflight gets 401 instead of CORS headers. This is consistent with Gin, Echo, and Chi.
```go
router.Use(middleware.CORS())
// With custom config
router.Use(middleware.CORSWithConfig(middleware.CORSConfig{
AllowOrigins: "https://example.com,https://foo.com",
AllowMethods: "GET,POST,PUT,DELETE",
AllowHeaders: "Content-Type,Authorization",
AllowCredentials: true,
MaxAge: 12 * time.Hour,
}))
```
**Features**:
- ✅ Wildcard origins (`*`) support
- ✅ Preflight requests (OPTIONS) handling
- ✅ Credentials support
- ✅ Expose headers configuration
- ✅ MaxAge caching
- ✅ Zero external dependencies
---
#### BasicAuth
HTTP Basic Authentication with constant-time comparison.
```go
router.Use(middleware.BasicAuth(middleware.BasicAuthConfig{
Username: "admin",
Password: "secret",
}))
// With custom validator
router.Use(middleware.BasicAuth(middleware.BasicAuthConfig{
Validator: func(username, password string) bool {
return checkDatabase(username, password)
},
}))
```
**Features**:
- ✅ Simple username/password validation
- ✅ Custom validator function
- ✅ Realm configuration
- ✅ WWW-Authenticate header
- ✅ Constant-time comparison (timing attack protection)
- ✅ Zero external dependencies
---
#### Secure
OWASP 2025 security headers for production hardening.
```go
router.Use(middleware.Secure())
// With custom config
router.Use(middleware.SecureWithConfig(middleware.SecureConfig{
ContentSecurityPolicy: "default-src 'self'; script-src 'self' 'unsafe-inline'",
HSTSMaxAge: 31536000, // 1 year
HSTSExcludeSubdomains: false,
XFrameOptions: "DENY",
ContentTypeNosniff: "nosniff",
ReferrerPolicy: "strict-origin-when-cross-origin",
}))
```
**Features (OWASP 2025)**:
- ✅ Content-Security-Policy (CSP)
- ✅ Strict-Transport-Security (HSTS)
- ✅ X-Frame-Options
- ✅ X-Content-Type-Options: nosniff
- ✅ X-XSS-Protection (deprecated, not set by default)
- ✅ Referrer-Policy
- ✅ Cross-Origin-Embedder-Policy
- ✅ Cross-Origin-Opener-Policy
- ✅ Cross-Origin-Resource-Policy
- ✅ Permissions-Policy
**Coverage**: 100%
**Dependencies**: Zero (stdlib only)
---
### Authentication & Rate Limiting
#### JWT
JWT token validation with algorithm confusion prevention.
```go
// Simple usage with signing key
router.Use(middleware.JWT([]byte("your-secret-key")))
// With full configuration
router.Use(middleware.JWTWithConfig(middleware.JWTConfig{
SigningKey: []byte("your-secret-key"),
SigningMethod: "HS256",
TokenLookup: "header:Authorization",
}))
// With issuer and audience validation
router.Use(middleware.JWTWithConfig(middleware.JWTConfig{
SigningKey: []byte("secret"),
SigningMethod: "HS256",
ValidateIssuer: "my-app",
ValidateAudience: "api",
}))
```
**Features**:
- ✅ Algorithms: HS256, HS384, HS512, RS256, ES256
- ✅ Token from Header/Query/Cookie
- ✅ Issuer/Audience validation
- ✅ Algorithm confusion prevention (forbids "none")
- ✅ Custom claims support
- ✅ Expiration time validation
**Dependency**: `github.com/golang-jwt/jwt/v5`
---
#### RateLimit
Token bucket rate limiting with RFC-compliant headers.
```go
// Simple usage (100 req/s, burst of 200)
router.Use(middleware.RateLimit(100, 200))
// With full configuration and custom key function
router.Use(middleware.RateLimitWithConfig(middleware.RateLimitConfig{
Rate: 10,
Burst: 20,
KeyFunc: func(c *fursy.Context) string {
// Rate limit by user ID
return c.GetString("user_id")
},
}))
```
**Features**:
- ✅ Token bucket algorithm (`golang.org/x/time/rate`)
- ✅ Per-IP or custom key function
- ✅ RFC headers (`X-RateLimit-Limit`, `X-RateLimit-Remaining`, `X-RateLimit-Reset`)
- ✅ In-memory store with automatic cleanup
- ✅ Custom error handler
- ✅ Configurable retry-after header
**Dependency**: `golang.org/x/time/rate`
---
### Resilience
#### CircuitBreaker
Zero-dependency circuit breaker for fault tolerance.
```go
// Simple usage with defaults (5 consecutive failures, 60s timeout)
router.Use(middleware.CircuitBreaker())
// With consecutive failures threshold
router.Use(middleware.CircuitBreakerConsecutive(5, 30*time.Second))
// With ratio-based threshold (3 failures out of 10 requests)
router.Use(middleware.CircuitBreakerRatio(3, 10, 30*time.Second))
// With full configuration
router.Use(middleware.CircuitBreakerWithConfig(middleware.CircuitBreakerConfig{
ConsecutiveFailures: 5,
Timeout: 30 * time.Second,
MaxRequests: 2, // Allow 2 requests in half-open state
}))
```
**Features**:
- ✅ Zero external dependencies (pure Go)
- ✅ Consecutive failures threshold
- ✅ Ratio-based threshold
- ✅ Time-window threshold
- ✅ Half-open state with max requests
- ✅ States: Closed → Open → Half-Open → Closed
- ✅ Custom error handler
- ✅ Thread-safe (concurrent request handling)
**Coverage**: 95.6%
**Dependencies**: Zero (stdlib only)
---
### Middleware Comparison
| Middleware | FURSY | Gin | Echo | Fiber |
|------------|-------|-----|------|-------|
| **Logger** | ✅ `log/slog` | ✅ Custom | ✅ Custom | ✅ Custom |
| **Recovery** | ✅ RFC 9457 | ✅ Basic | ✅ Basic | ✅ Basic |
| **CORS** | ✅ Built-in (zero deps) | 🔧 Plugin | 🔧 Plugin | ✅ Built-in |
| **BasicAuth** | ✅ Built-in | ✅ Built-in | ✅ Built-in | ✅ Built-in |
| **JWT** | ✅ Built-in | 🔧 Plugin | 🔧 Plugin | ✅ Built-in |
| **Rate Limit** | ✅ Built-in (RFC headers) | 🔧 Plugin | 🔧 Plugin | ✅ Built-in |
| **Security Headers** | ✅ OWASP 2025 | ❌ | 🔧 Plugin | ✅ Basic |
| **Circuit Breaker** | ✅ Zero deps | ❌ | ❌ | ❌ |
| **Test Coverage** | **94.6%** | ? | ? | ? |
| **Dependencies** | **Core: 0, JWT: 1, RateLimit: 1** | Multiple | Multiple | Multiple |
**Legend**:
- ✅ = Built-in with high quality implementation
- 🔧 = Plugin/third-party required
- ❌ = Not available
**FURSY advantage**: Production-ready middleware with minimal dependencies, OWASP 2025 compliance, RFC 9457 error responses, and comprehensive test coverage.
---
### Learn More
- **[Middleware Examples](examples/05-middleware/)** - Complete examples for all 8 middleware
- **[Middleware Source](middleware/)** - Middleware implementations with tests
---
## 🔀 Trailing Slash Handling
By default, `/users` and `/users/` are treated as different routes. Configure trailing slash handling to match your needs:
```go
// Strip trailing slashes silently (recommended for APIs behind reverse proxies)
router := fursy.New()
router.WithTrailingSlash(fursy.StripTrailingSlash)
router.Handle("GET", "/api/companies", handler)
// GET /api/companies → 200 (exact match)
// GET /api/companies/ → 200 (trailing slash stripped silently)
```
```go
// Redirect to canonical URL (SEO-friendly for public APIs)
router := fursy.New()
router.WithTrailingSlash(fursy.RedirectTrailingSlash)
router.Handle("GET", "/api/companies", handler)
// GET /api/companies/ → 301 redirect to /api/companies
// POST /api/companies/ → 308 redirect (preserves HTTP method)
```
**Bidirectional**: If `/files/` is the registered route and `/files` is requested, the router handles it too — redirecting or serving the `/files/` handler.
**Three modes**:
| Mode | Behavior |
|------|----------|
| `IgnoreTrailingSlash` (default) | `/users` and `/users/` are different routes |
| `StripTrailingSlash` | Silently serves handler without redirect |
| `RedirectTrailingSlash` | 301 (GET) or 308 (other methods) redirect |
---
## 🎯 Convenience Methods (REST Best Practices)
FURSY provides convenient shortcuts for common HTTP response patterns, following REST best practices:
### Context Convenience Methods
```go
// GET - 200 OK (most common) — plain handler via Handle()
router.Handle("GET", "/users", func(c *fursy.Context) error {
users := getAllUsers()
return c.OK(users) // Short for c.JSON(200, users)
})
// POST - 201 Created (resource creation) — plain handler via Handle()
router.Handle("POST", "/users", func(c *fursy.Context) error {
user := createUser(c)
return c.Created(user) // 201, not 200!
})
// DELETE - 204 No Content (successful deletion) — plain handler via Handle()
router.Handle("DELETE", "/users/:id", func(c *fursy.Context) error {
deleteUser(c.Param("id"))
return c.NoContentSuccess() // 204, no body
})
// Async operations - 202 Accepted — plain handler via Handle()
router.Handle("POST", "/jobs", func(c *fursy.Context) error {
jobID := startAsyncJob(c)
return c.Accepted(map[string]string{"jobId": jobID})
})
// Simple text - 200 OK — plain handler via Handle()
router.Handle("GET", "/ping", func(c *fursy.Context) error {
return c.Text("pong") // text/plain, 200
})
```
**Why use convenience methods?**
- ✅ **Less boilerplate** - `c.OK(data)` vs `c.JSON(200, data)`
- ✅ **REST semantics** - `Created()` clearly indicates 201, preventing mistakes
- ✅ **Self-documenting** - Code intent is clear from method name
- ✅ **Flexibility** - Original methods still available for custom status codes
**For custom status codes**, use explicit methods:
```go
// Partial content - 206
return c.JSON(206, partialData)
// Custom redirect - 307
return c.Redirect(307, "/new-location")
```
### Box Convenience Methods (Type-Safe)
```go
// GET - 200 OK (type parameters inferred from handler signature)
router.GET("/users/:id", func(b *fursy.Box[GetUserRequest, UserResponse]) error {
user := getUser(b.ReqBody.ID)
return b.OK(user) // Type-safe 200 OK
})
// POST - 201 Created with Location header
router.POST("/users", func(b *fursy.Box[CreateUserRequest, UserResponse]) error {
user := createUser(b.ReqBody)
return b.Created("/users/"+user.ID, user) // 201 + Location
})
// PUT - 200 OK with body
router.PUT("/users/:id", func(b *fursy.Box[UpdateUserRequest, UserResponse]) error {
updated := updateUser(b.ReqBody)
return b.UpdatedOK(updated) // Semantic clarity
})
// PUT - 204 No Content (no response body)
router.PUT("/users/:id/status", func(b *fursy.Box[UpdateUserRequest, Empty]) error {
updateUser(b.ReqBody)
return b.UpdatedNoContent() // 204, no body
})
// DELETE - 204 No Content
router.DELETE("/users/:id", func(b *fursy.Box[Empty, Empty]) error {
deleteUser(b.Param("id"))
return b.NoContentSuccess() // 204
})
```
### Plugin Integration Methods
FURSY provides seamless integration with plugins through convenient Context methods:
#### Database Access
```go
import (
"github.com/coregx/fursy"
"github.com/coregx/fursy/plugins/database"
)
// Setup database
sqlDB, _ := sql.Open("postgres", dsn)
db := database.NewDB(sqlDB)
router := fursy.New()
router.Use(database.Middleware(db))
// Access database in handlers
router.Handle("GET", "/users/:id", func(c *fursy.Context) error {
db, ok := database.GetDB(c)
if !ok {
return c.Problem(fursy.InternalServerError("Database not configured"))
}
var user User
err := db.QueryRow(c.Request.Context(),
"SELECT id, name FROM users WHERE id = $1", c.Param("id")).
Scan(&user.ID, &user.Name)
if err == sql.ErrNoRows {
return c.Problem(fursy.NotFound("User not found"))
}
return c.JSON(200, user)
})
```
#### Server-Sent Events (SSE)
```go
import (
"github.com/coregx/fursy/plugins/stream"
"github.com/coregx/stream/sse"
)
// Setup SSE hub
hub := sse.NewHub[Notification]()
go hub.Run()
defer hub.Close()
router.Use(stream.SSEHub(hub))
// SSE endpoint
router.Handle("GET", "/events", func(c *fursy.Context) error {
hub, _ := stream.GetSSEHub[Notification](c)
return stream.SSEUpgrade(c, func(conn *sse.Conn) error {
hub.Register(conn)
defer hub.Unregister(conn)
<-conn.Done() // Wait for client disconnect
return nil
})
})
```
#### WebSocket
```go
import (
"github.com/coregx/fursy/plugins/stream"
"github.com/coregx/stream/websocket"
)
// Setup WebSocket hub
hub := websocket.NewHub()
go hub.Run()
defer hub.Close()
router.Use(stream.WebSocketHub(hub))
// WebSocket endpoint
router.Handle("GET", "/ws", func(c *fursy.Context) error {
hub, _ := stream.GetWebSocketHub(c)
return stream.WebSocketUpgrade(c, func(conn *websocket.Conn) error {
hub.Register(conn)Frequently Asked Questions
What is fursy?
fursy is a Web Frameworks library for the Go programming language. HTTP router with type-safe generic handlers, automatic OpenAPI 3.1 generation from code, and RFC 9457 error responses
How do I install fursy?
Install fursy with the Go module system using `go get coregx/fursy`. Check the repository for the current installation instructions.
What category does fursy belong to?
fursy is listed under Web Frameworks, specifically Routers.