Awesome Go

fursy

CategoryWeb Frameworks
SubcategoryRouters
Stars12

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.

[![Go Reference](https://pkg.go.dev/badge/github.com/coregx/fursy.svg)](https://pkg.go.dev/github.com/coregx/fursy)
[![Go Report Card](https://goreportcard.com/badge/github.com/coregx/fursy)](https://goreportcard.com/report/github.com/coregx/fursy)
[![Tests](https://github.com/coregx/fursy/actions/workflows/test.yml/badge.svg)](https://github.com/coregx/fursy/actions)
[![codecov](https://codecov.io/gh/coregx/fursy/branch/main/graph/badge.svg)](https://codecov.io/gh/coregx/fursy)
[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
[![Release](https://img.shields.io/github/v/release/coregx/fursy)](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.

← Back to Routers