DEV Community

Cover image for FURSY: The First Type-Safe HTTP Router for Go - 10M req/s with Zero Runtime Errors
Andrey Kolkov
Andrey Kolkov

Posted on • Edited on • Originally published at github.com

FURSY: The First Type-Safe HTTP Router for Go - 10M req/s with Zero Runtime Errors

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!
}
Enter fullscreen mode Exit fullscreen mode

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)
}
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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"
    }
}
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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)
    }
}
Enter fullscreen mode Exit fullscreen mode

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)
}
Enter fullscreen mode Exit fullscreen mode

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,
}))
Enter fullscreen mode Exit fullscreen mode

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})
})
Enter fullscreen mode Exit fullscreen mode

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)
    },
}))
Enter fullscreen mode Exit fullscreen mode

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()})
    })
}
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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)
})
Enter fullscreen mode Exit fullscreen mode

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)
Enter fullscreen mode Exit fullscreen mode

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"`
}
Enter fullscreen mode Exit fullscreen mode

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",
    },
}
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
    })
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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)