TL;DR: I built fursy, a Go HTTP router that catches API errors at compile time instead of production. Type-safe generics, 256 ns/op routing, RFC 9457 errors, OpenAPI generation - all with minimal dependencies. Here's why it matters and how to use it.
The Problem Every Go Developer Faces
Look at this typical Gin/Echo handler:
func CreateUser(c *gin.Context) {
var req CreateUserRequest
if err := c.BindJSON(&req); err != nil {
c.JSON(400, gin.H{"error": err.Error()})
return
}
// What if req has wrong types? Runtime panic.
// What if you forget a field? Runtime error.
// What if validation fails? Hope you catch it!
}
Every mistake is a runtime error. You find bugs in production, not during compilation.
I've been building HTTP services in Go for years, and this pattern has always bothered me. Go has generics since 1.18. Why are we still writing handlers that discover type errors at runtime?
So I built FURSY (Fast Universal Routing SYstem) - the first Go router where API contract violations are compile-time errors.
What Makes FURSY Different
Type-Safe Handlers with Box[Req, Res]
type CreateUserRequest struct {
Email string `json:"email" validate:"required,email"`
Username string `json:"username" validate:"required,min=3,max=50"`
Age int `json:"age" validate:"gte=18,lte=120"`
Password string `json:"password" validate:"required,min=8"`
}
type UserResponse struct {
ID int `json:"id"`
Username string `json:"username"`
Email string `json:"email"`
}
// Compile-time type safety!
fursy.POST[CreateUserRequest, UserResponse](router, "/users", createUser)
func createUser(c *fursy.Box[CreateUserRequest, UserResponse]) error {
// ReqBody is AUTOMATICALLY bound and validated!
// No manual Bind() call needed - it happens in the generic adapter.
// IDE autocomplete works. Compiler catches mistakes.
user := UserResponse{
ID: 1,
Username: c.ReqBody.Username,
Email: c.ReqBody.Email,
}
return c.Created("/users/1", user)
}
Key insight: Box[Req, Res] is a generic context. The compiler knows exactly what request body you expect and what response you'll return. Change a field name? Compiler error. Wrong type? Compiler error. This is how HTTP APIs should work in a typed language.
Performance: 256 ns/op Routing
BenchmarkRouter_StaticRoute-12 4628498 256.3 ns/op 1 allocs/op
BenchmarkRouter_ParamRoute-12 3676854 326.4 ns/op 1 allocs/op
BenchmarkRouter_DeepNesting-12 2135762 561.2 ns/op 1 allocs/op
That's ~10 million requests per second on a single core for static routes. The radix tree implementation achieves near-zero allocation routing with 1 alloc/op (for the context).
RFC 9457 Problem Details (Built-in)
When validation fails, fursy returns standardized error responses:
{
"type": "about:blank",
"title": "Validation Failed",
"status": 422,
"detail": "3 field(s) failed validation",
"errors": {
"email": "Email must be a valid email address",
"username": "Username must be at least 3 characters",
"password": "Password is required"
}
}
No more inventing error formats. RFC 9457 is the standard. Every client knows how to parse it.
Minimal Dependencies
Core package: Zero external dependencies (stdlib only)
Middleware (justified exceptions):
-
golang-jwt/jwt/v5- JWT authentication -
golang.org/x/time- Rate limiting
Plugins (separate modules): OpenTelemetry, Validator, Stream (SSE/WebSocket), Database
Compare this to Gin (8 deps), Echo (6 deps), or Fiber (11 deps). Fewer dependencies = fewer security vulnerabilities = easier auditing.
Quick Start
Installation
go get github.com/coregx/fursy@latest
Requires Go 1.25+ (uses encoding/json/v2, log/slog, and advanced generics).
Hello World
package main
import (
"log/slog"
"net/http"
"os"
"github.com/coregx/fursy"
)
func main() {
router := fursy.New()
router.GET("/", func(c *fursy.Context) error {
return c.OK(map[string]string{
"message": "Hello, World!",
"status": "success",
})
})
slog.Info("Server starting", "port", 8080)
if err := http.ListenAndServe(":8080", router); err != nil {
slog.Error("Server failed", "error", err)
os.Exit(1)
}
}
Type-Safe CRUD API
package main
import (
"net/http"
"github.com/coregx/fursy"
"github.com/coregx/fursy/middleware"
"github.com/coregx/fursy/plugins/validator"
)
// Request/Response types with validation
type CreateUserRequest struct {
Email string `json:"email" validate:"required,email"`
Username string `json:"username" validate:"required,min=3,max=50"`
Age int `json:"age" validate:"gte=18,lte=120"`
Password string `json:"password" validate:"required,min=8"`
}
type UserResponse struct {
ID int `json:"id"`
Username string `json:"username"`
Email string `json:"email"`
}
type GetUserRequest struct {
ID string `param:"id" validate:"required"`
}
func main() {
router := fursy.New()
// Enable automatic validation
router.SetValidator(validator.New())
// Global middleware
router.Use(middleware.Logger())
router.Use(middleware.Recovery())
// Type-safe endpoints
fursy.POST[CreateUserRequest, UserResponse](router, "/users", createUser)
fursy.GET[GetUserRequest, UserResponse](router, "/users/:id", getUser)
http.ListenAndServe(":8080", router)
}
func createUser(c *fursy.Box[CreateUserRequest, UserResponse]) error {
// ReqBody is automatically bound and validated!
// If validation fails, RFC 9457 error is returned before handler runs.
user := UserResponse{
ID: 1,
Username: c.ReqBody.Username,
Email: c.ReqBody.Email,
}
return c.Created("/users/1", user)
}
func getUser(c *fursy.Box[GetUserRequest, UserResponse]) error {
// ReqBody.ID is automatically validated from URL param!
user := UserResponse{
ID: 1,
Username: "john",
Email: "john@example.com",
}
return c.OK(user)
}
Production-Ready Middleware
FURSY includes 8 battle-tested middleware:
Essential Stack (Apply in This Order!)
router := fursy.New()
// 1. Recovery - Catch panics first
router.Use(middleware.Recovery())
// 2. Logger - Log everything
router.Use(middleware.Logger())
// 3. Secure - OWASP 2025 security headers
router.Use(middleware.SecureWithConfig(middleware.SecureConfig{
XFrameOptions: middleware.XFrameOptionsDeny,
ContentTypeNosniff: middleware.ContentTypeNosniffValue,
ReferrerPolicy: middleware.ReferrerPolicyStrictOrigin,
HSTSMaxAge: 31536000, // 1 year
ContentSecurityPolicy: "default-src 'self'",
}))
// 4. CORS - For API endpoints
router.Use(middleware.CORSWithConfig(middleware.CORSConfig{
AllowOrigins: "https://app.example.com",
AllowMethods: "GET,POST,PUT,DELETE",
AllowHeaders: "Content-Type,Authorization",
AllowCredentials: true,
}))
Authentication
import "github.com/golang-jwt/jwt/v5"
// JWT Authentication
protected := router.Group("/api")
protected.Use(middleware.JWTWithConfig(middleware.JWTConfig{
SigningKey: []byte("your-secret-key"),
SigningMethod: "HS256",
TokenLookup: "header:Authorization",
AuthScheme: "Bearer",
}))
// Access claims in handler
protected.GET("/profile", func(c *fursy.Context) error {
claims := c.Get(middleware.JWTContextKey).(jwt.MapClaims)
userID := claims["sub"].(string)
return c.OK(map[string]string{"user_id": userID})
})
Rate Limiting & Circuit Breaker
// Rate limiting (10 req/s with burst of 20)
api.Use(middleware.RateLimitWithConfig(middleware.RateLimitConfig{
Rate: 10,
Burst: 20,
Headers: true, // X-RateLimit-* headers
KeyFunc: func(c *fursy.Context) string {
return c.Request.RemoteAddr
},
}))
// Circuit breaker for fault tolerance
api.Use(middleware.CircuitBreakerWithConfig(middleware.CircuitBreakerConfig{
ConsecutiveFailures: 5,
Timeout: 30 * time.Second,
MaxRequests: 3,
OnStateChange: func(from, to middleware.State) {
slog.Warn("Circuit breaker", "from", from, "to", to)
},
}))
Real-Time Features: SSE & WebSocket
FURSY includes a stream plugin for real-time communication:
import (
"time"
"github.com/coregx/fursy"
"github.com/coregx/fursy/plugins/stream"
"github.com/coregx/stream/sse"
)
type Notification struct {
Type string `json:"type"`
Message string `json:"message"`
Time time.Time `json:"time"`
}
func main() {
// Create SSE Hub
hub := sse.NewHub[Notification]()
go hub.Run()
defer hub.Close()
router := fursy.New()
router.Use(stream.SSEHub(hub))
// SSE endpoint - clients connect here
router.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)
// Send welcome message
conn.SendJSON(Notification{
Type: "info",
Message: "Connected!",
Time: time.Now(),
})
<-conn.Done() // Wait for disconnect
return nil
})
})
// Broadcast endpoint
router.POST("/notify", func(c *fursy.Context) error {
hub, _ := stream.GetSSEHub[Notification](c)
hub.BroadcastJSON(Notification{
Type: "alert",
Message: "Important update!",
Time: time.Now(),
})
return c.OK(map[string]int{"clients": hub.Clients()})
})
}
Connect with: curl -N http://localhost:8080/events
Route Groups & API Versioning
router := fursy.New()
// API v1
v1 := router.Group("/api/v1")
v1.Use(middleware.Logger())
{
// Public endpoints
v1.GET("/health", healthCheck)
// Protected endpoints
users := v1.Group("/users")
users.Use(middleware.JWT(secret))
{
fursy.GET[GetUserRequest, UserResponse](users, "/:id", getUser)
fursy.POST[CreateUserRequest, UserResponse](users, "", createUser)
fursy.PUT[UpdateUserRequest, UserResponse](users, "/:id", updateUser)
fursy.DELETE[DeleteUserRequest, fursy.Empty](users, "/:id", deleteUser)
}
}
// API v2 (with different validation)
v2 := router.Group("/api/v2")
// ... different handlers
OpenAPI 3.1 Generation
Generate OpenAPI spec automatically from your code:
// Generate OpenAPI 3.1 spec from registered routes
spec, err := router.GenerateOpenAPI(fursy.Info{
Title: "My API",
Version: "1.0.0",
Description: "Production API",
})
if err != nil {
log.Fatal(err)
}
// Serve spec as JSON
router.GET("/openapi.json", func(c *fursy.Context) error {
return spec.WriteJSON(c.Response)
})
The spec is generated from your type definitions - Box[Req, Res] types become request/response schemas with validation rules.
Why FURSY Over Gin/Echo/Fiber?
| Feature | fursy | Gin | Echo | Fiber |
|---|---|---|---|---|
| Type Safety | Compile-time | Runtime | Runtime | Runtime |
| Validation | Automatic | Manual | Manual | Manual |
| RFC 9457 Errors | Built-in | Custom | Custom | Custom |
| OpenAPI Gen | Built-in | Plugin | Plugin | Plugin |
| Dependencies | 0 (core) | 8 | 6 | 11 |
| Performance | 256 ns/op | ~300 ns/op | ~350 ns/op | ~100 ns/op |
| Go Version | 1.25+ | 1.13+ | 1.17+ | 1.17+ |
Trade-off: FURSY requires Go 1.25+ for encoding/json/v2 and advanced generics. If you're stuck on older Go, Gin/Echo are fine choices.
But if you can use Go 1.25+: FURSY gives you type safety that no other router offers. Every API error becomes a compile error.
Best Practices for LLMs & AI Coding Agents
If you're using Copilot, Claude, or other AI assistants with FURSY:
1. Always Define Request/Response Types First
// Define types BEFORE writing handlers
type CreateOrderRequest struct {
ProductID string `json:"product_id" validate:"required,uuid"`
Quantity int `json:"quantity" validate:"required,gte=1,lte=100"`
Notes string `json:"notes,omitempty" validate:"max=500"`
}
type OrderResponse struct {
ID string `json:"id"`
Status string `json:"status"`
CreatedAt time.Time `json:"created_at"`
}
// Then register handler with types
fursy.POST[CreateOrderRequest, OrderResponse](router, "/orders", createOrder)
2. Use Validation Tags Exhaustively
type UserRequest struct {
Email string `json:"email" validate:"required,email"`
Username string `json:"username" validate:"required,min=3,max=50,alphanum"`
Age int `json:"age" validate:"omitempty,gte=0,lte=150"`
Website string `json:"website" validate:"omitempty,url"`
Role string `json:"role" validate:"required,oneof=admin user guest"`
}
3. Handle Errors with Problem Details
// Built-in helpers
return fursy.NotFound("User not found")
return fursy.BadRequest("Invalid input")
return fursy.Unauthorized("Token expired")
// Custom problems
return fursy.Problem{
Type: "https://api.example.com/errors/quota-exceeded",
Title: "Quota Exceeded",
Status: 429,
Detail: "You've used 100% of your monthly quota",
Extensions: map[string]any{
"quota_limit": 1000,
"quota_used": 1000,
"resets_at": "2025-02-01T00:00:00Z",
},
}
4. Middleware Order Matters
// CORRECT order
router.Use(middleware.Recovery()) // 1. Catch panics
router.Use(middleware.Logger()) // 2. Log everything
router.Use(middleware.Secure()) // 3. Security headers
router.Use(middleware.CORS()) // 4. CORS before auth
router.Use(middleware.RateLimit(10, 20)) // 5. Rate limit (10 req/s, burst 20)
router.Use(middleware.JWT([]byte("your-secret"))) // 6. Auth last
5. Use fursy.Empty for No-Body Endpoints
// DELETE with no request/response body
fursy.DELETE[fursy.Empty, fursy.Empty](router, "/users/:id",
func(c *fursy.Box[fursy.Empty, fursy.Empty]) error {
id := c.Param("id")
deleteUser(id)
return c.NoContentSuccess() // 204 No Content
})
Project Stats
- Version: v0.3.3 (Production Ready)
- Test Coverage: 91.7% (650+ tests)
- Benchmarks: 42 benchmarks
- Linter: 0 issues (golangci-lint strict mode)
- Dependencies: 2 (JWT + rate limit)
- Performance: 256 ns/op static, 326 ns/op parametric
Getting Started
# Install
go get github.com/coregx/fursy@latest
# Optional plugins
go get github.com/coregx/fursy/plugins/validator@latest
go get github.com/coregx/fursy/plugins/stream@latest
Repository: github.com/coregx/fursy
Examples: Check examples/ directory - 11 complete examples from hello-world to production boilerplate.
Documentation: llms.md in repository root - comprehensive guide for AI agents.
Go Report Card: A+
What's Next
FURSY is in Phase 4 (Ecosystem):
- More plugins (database, cache, gRPC gateway)
- Documentation website
- Migration guides from Gin/Echo
- Community building
If you're building Go APIs and want compile-time safety instead of runtime errors, give FURSY a try. It's how HTTP routers should work in a typed language.
Questions? Issues? PRs welcome at github.com/coregx/fursy!
Top comments (0)