fursy

package module
v0.3.3 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Nov 24, 2025 License: MIT Imports: 19 Imported by: 2

README

🔥 FURSY

Fast Universal Routing System

Next-generation HTTP router for Go with blazing performance, type-safe handlers, and minimal dependencies.

Go Reference Go Report Card Tests codecov Release


⚡ Quick Start

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 with convenience method
    router.GET("/", func(c *fursy.Context) error {
        return c.Text("Welcome to FURSY!")  // 200 OK
    })

    // GET with convenience method (200 OK)
    router.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 convenience method (201 Created)
    router.POST("/users", func(c *fursy.Context) error {
        username := c.Form("username")
        email := c.Form("email")

        user := map[string]string{
            "id":    "123",
            "name":  username,
            "email": email,
        }
        return c.Created(user)  // 201 Created - REST best practice!
    })

    // DELETE with convenience method (204 No Content)
    router.DELETE("/users/:id", func(c *fursy.Context) error {
        // Delete user...
        return c.NoContentSuccess()  // 204 No Content
    })

    // Query parameters
    router.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: The examples above use simple handlers with *Context. For type-safe generic handlers Box[Req, Res], see the Type-Safe Handlers section below!


🌟 Why FURSY?

Type-Safe Handlers (First in Go!)
func Handler(box *fursy.Box[Request, Response]) error {
    // Compile-time type safety
    // Automatic validation
    // Zero boilerplate
}
Native RFC 9457 Problem Details
{
  "type": "https://fursy.coregx.dev/problems/validation-error",
  "title": "Validation Failed",
  "status": 400,
  "errors": [...]
}
Built-in OpenAPI 3.1 Generation
spec := r.OpenAPI(fursy.OpenAPIConfig{
    Title: "My API",
    Version: "1.0.0",
})
// Complete OpenAPI 3.1 spec from code!
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
  • 256 ns/op static routes, 326 ns/op parametric routes
  • 1 allocation/op (routing hot path)
  • ~10M req/s throughput (simple routes)
  • Zero-allocation radix tree routing
  • Efficient context pooling

📦 Installation

go get github.com/coregx/fursy

Requirements: Go 1.25+


🚀 Features

  • High Performance Routing - 256-326 ns/op, 1 alloc/op
  • Type-Safe Generic Handlers - Box[Req, Res] with compile-time safety
  • 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
  • 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.

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.

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).

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.

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.

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.

import "github.com/golang-jwt/jwt/v5"

router.Use(middleware.JWT(middleware.JWTConfig{
    SigningKey:    []byte("your-secret-key"),
    SigningMethod: jwt.SigningMethodHS256,
    TokenLookup:   "header:Authorization",
}))

// With custom validation
router.Use(middleware.JWT(middleware.JWTConfig{
    SigningKey:    []byte("secret"),
    SigningMethod: jwt.SigningMethodHS256,
    Issuer:        "my-app",
    Audience:      []string{"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.

router.Use(middleware.RateLimit(middleware.RateLimitConfig{
    Rate:  100,  // 100 requests per second
    Burst: 200,  // burst of 200
    KeyFunc: middleware.RateLimitByIP,
}))

// Custom key function
router.Use(middleware.RateLimit(middleware.RateLimitConfig{
    Rate:  10,
    Burst: 20,
    KeyFunc: func(c *fursy.Context) string {
        // Rate limit by user ID
        userID := c.Get("user_id").(string)
        return userID
    },
}))

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.

router.Use(middleware.CircuitBreaker(middleware.CircuitBreakerConfig{
    MaxRequests:         100,
    ConsecutiveFailures: 5,
    Timeout:             30 * time.Second,
    ResetTimeout:        60 * time.Second,
}))

// With ratio-based threshold
router.Use(middleware.CircuitBreaker(middleware.CircuitBreakerConfig{
    MaxRequests:  1000,
    FailureRatio: 0.25, // Open circuit when 25% of requests fail
    Timeout:      30 * time.Second,
    ResetTimeout: 60 * time.Second,
}))

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.5% 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 93.1% ? ? ?
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

🎯 Convenience Methods (REST Best Practices)

FURSY provides convenient shortcuts for common HTTP response patterns, following REST best practices:

Context Convenience Methods
// GET - 200 OK (most common)
router.GET("/users", func(c *fursy.Context) error {
    users := getAllUsers()
    return c.OK(users)  // Short for c.JSON(200, users)
})

// POST - 201 Created (resource creation)
router.POST("/users", func(c *fursy.Context) error {
    user := createUser(c)
    return c.Created(user)  // 201, not 200!
})

// DELETE - 204 No Content (successful deletion)
router.DELETE("/users/:id", func(c *fursy.Context) error {
    deleteUser(c.Param("id"))
    return c.NoContentSuccess()  // 204, no body
})

// Async operations - 202 Accepted
router.POST("/jobs", func(c *fursy.Context) error {
    jobID := startAsyncJob(c)
    return c.Accepted(map[string]string{"jobId": jobID})
})

// Simple text - 200 OK
router.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:

// Partial content - 206
return c.JSON(206, partialData)

// Custom redirect - 307
return c.Redirect(307, "/new-location")
Box Convenience Methods (Type-Safe)
// GET - 200 OK
router.GET[GetUserRequest, UserResponse]("/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[CreateUserRequest, UserResponse]("/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[UpdateUserRequest, UserResponse]("/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[UpdateUserRequest, Empty]("/users/:id", func(b *fursy.Box[UpdateUserRequest, Empty]) error {
    updateUser(b.ReqBody)
    return b.UpdatedNoContent()  // 204, no body
})

// DELETE - 204 No Content
router.DELETE[Empty, Empty]("/users/:id", func(b *fursy.Box[Empty, Empty]) error {
    deleteUser(c.Param("id"))
    return b.NoContentSuccess()  // 204
})
Plugin Integration Methods

FURSY provides seamless integration with plugins through convenient Context methods:

Database Access
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.GET("/users/:id", func(c *fursy.Context) error {
    db := c.DB().(*database.DB)  // Type assertion

    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)
})

Type-safe helper (recommended):

router.GET("/users/:id", func(c *fursy.Context) error {
    db, ok := database.GetDB(c)  // Type-safe retrieval
    if !ok {
        return c.Problem(fursy.InternalServerError("Database not configured"))
    }

    // Use db...
})
Server-Sent Events (SSE)
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.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
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.GET("/ws", func(c *fursy.Context) error {
    hub, _ := stream.GetWebSocketHub(c)

    return stream.WebSocketUpgrade(c, func(conn *websocket.Conn) error {
        hub.Register(conn)
        defer hub.Unregister(conn)

        for {
            msgType, data, err := conn.Read()
            if err != nil {
                return err
            }
            hub.Broadcast(data)  // Echo to all clients
        }
    }, nil)
})

See also:


🎯 Automatic Validation

FURSY provides type-safe automatic validation through the validator plugin, giving you compile-time type safety combined with runtime validation - a unique combination in the Go ecosystem.

Why FURSY Validation is Different

Traditional routers require manual validation on every handler:

// ❌ Manual validation (Gin, Echo, Fiber)
func CreateUser(c *gin.Context) {
    var req CreateUserRequest
    if err := c.BindJSON(&req); err != nil {  // No validation!
        c.JSON(400, gin.H{"error": err.Error()})
        return
    }

    // Manual validation needed
    if req.Email == "" || !isValidEmail(req.Email) {
        c.JSON(400, gin.H{"error": "invalid email"})
        return
    }
    // ... repeat for every field
}

With FURSY's type-safe handlers, validation is automatic and guaranteed:

// ✅ Automatic validation (FURSY)
router.POST[CreateUserRequest, UserResponse]("/users",
    func(c *fursy.Box[CreateUserRequest, UserResponse]) error {
        if err := c.Bind(); err != nil {
            return err  // Automatic RFC 9457 error response
        }

        // c.ReqBody is ALREADY validated! ✅
        user := createUser(c.ReqBody)
        return c.Created("/users/"+user.ID, user)
    })

Key advantages:

  • Set once, validate everywhere - No manual checks per handler
  • Compile-time type safety - Generics ensure request/response types match
  • RFC 9457 compliant - Standard error format with field-level details
  • 100+ validation tags - email, URL, UUID, min/max, and more
Quick Example
package main

import (
    "github.com/coregx/fursy"
    "github.com/coregx/fursy/plugins/validator"
)

type CreateUserRequest struct {
    Name  string `json:"name" validate:"required,min=3,max=50"`
    Email string `json:"email" validate:"required,email"`
    Age   int    `json:"age" validate:"required,gte=18,lte=120"`
}

type UserResponse struct {
    ID    string `json:"id"`
    Name  string `json:"name"`
    Email string `json:"email"`
}

func main() {
    router := fursy.New()

    // Set validator once - applies to ALL handlers
    router.SetValidator(validator.New())

    // Type-safe handler with automatic validation
    router.POST[CreateUserRequest, UserResponse]("/users",
        func(c *fursy.Box[CreateUserRequest, UserResponse]) error {
            if err := c.Bind(); err != nil {
                return err  // Automatic RFC 9457 response
            }

            // c.ReqBody is validated and type-safe!
            user := createUser(c.ReqBody)
            return c.Created("/users/"+user.ID, user)
        })

    router.Run(":8080")
}
Validation Error Response

When validation fails, FURSY returns RFC 9457 Problem Details with field-level errors:

Request:

curl -X POST http://localhost:8080/users \
  -H "Content-Type: application/json" \
  -d '{"name":"Jo","email":"invalid","age":15}'

Response (422 Unprocessable Entity):

{
  "type": "about:blank",
  "title": "Validation Failed",
  "status": 422,
  "detail": "3 field(s) failed validation",
  "errors": {
    "name": "Name must be at least 3 characters",
    "email": "Email must be a valid email address",
    "age": "Age must be 18 or greater"
  }
}
Comparison with Other Routers
Feature FURSY Gin Echo Fiber
Type Safety ✅ Compile-time (Box[Req, Res]) ❌ Runtime only ❌ Runtime only ❌ Runtime only
Auto Validation ✅ Set once, validate all ❌ Manual per handler ❌ Manual per handler ❌ Manual per handler
Error Format ✅ RFC 9457 (standard) ❌ Custom JSON ❌ Custom JSON ❌ Custom JSON
Setup Complexity ✅ One line (SetValidator) ❌ Validator + binding per route ❌ Validator + binding per route ❌ Validator + binding per route
Field-Level Errors ✅ Automatic 🔧 Manual mapping 🔧 Manual mapping 🔧 Manual mapping

Learn more: See Validator Plugin Documentation for custom validators, nested structs, and advanced features.


🌐 Content Negotiation

FURSY provides RFC 9110 compliant content negotiation, enabling your API to respond in multiple formats based on the client's Accept header. This is essential for building modern APIs that serve both humans (HTML/Markdown) and machines (JSON/XML).

Why Content Negotiation Matters

Modern APIs need to support multiple clients:

  • Web Browsers → HTML
  • API Clients → JSON
  • AI Agents → Markdown (for better understanding)
  • Legacy Systems → XML

FURSY handles this automatically using RFC 9110 standards with quality values (q-parameters).

Automatic Format Selection

The simplest approach - FURSY picks the best format automatically:

router.GET("/users/:id", func(c *fursy.Context) error {
    user := getUser(c.Param("id"))

    // Automatically selects format based on Accept header
    // Supports: JSON, HTML, XML, Text, Markdown
    return c.Negotiate(200, user)
})

Client requests:

# JSON (default)
curl http://localhost:8080/users/123
# → Content-Type: application/json

# HTML
curl -H "Accept: text/html" http://localhost:8080/users/123
# → Content-Type: text/html

# XML
curl -H "Accept: application/xml" http://localhost:8080/users/123
# → Content-Type: application/xml
Explicit Format Control

For finer control, check what the client accepts:

router.GET("/docs", func(c *fursy.Context) error {
    // Check if client accepts markdown
    if c.Accepts(fursy.MIMETextMarkdown) {
        docs := generateMarkdownDocs()
        return c.Markdown(docs)  // AI-friendly format
    }

    // Fallback to JSON
    return c.OK(map[string]string{"message": "Use Accept: text/markdown for docs"})
})
Quality Values (q-parameter)

RFC 9110 defines quality values to prioritize formats:

router.GET("/api/data", func(c *fursy.Context) error {
    data := getData()

    // Client sends: Accept: text/html;q=0.9, application/json;q=1.0
    // FURSY automatically picks JSON (higher q-value)
    format := c.AcceptsAny(
        fursy.MIMEApplicationJSON,  // q=1.0
        fursy.MIMETextHTML,          // q=0.9
        fursy.MIMETextMarkdown,      // fallback
    )

    switch format {
    case fursy.MIMEApplicationJSON:
        return c.JSON(200, data)
    case fursy.MIMETextHTML:
        return c.HTML(200, renderHTML(data))
    case fursy.MIMETextMarkdown:
        return c.Markdown(formatMarkdown(data))
    default:
        return c.OK(data)  // Default to JSON
    }
})
Supported Formats
Format MIME Type Constant Use Case
JSON application/json MIMEApplicationJSON API responses (default)
HTML text/html MIMETextHTML Web browsers
XML application/xml MIMEApplicationXML Legacy systems
Plain Text text/plain MIMETextPlain Simple data
Markdown text/markdown MIMETextMarkdown AI agents, documentation
AI Agent Support

FURSY has first-class support for AI agents via Markdown responses:

router.GET("/api/schema", func(c *fursy.Context) error {
    // AI agents prefer markdown for better understanding
    if c.Accepts(fursy.MIMETextMarkdown) {
        schema := `
# API Schema

## Users Endpoint
- **GET** /users - List all users
- **POST** /users - Create new user
  - Required: name (string), email (string)

## Authentication
All endpoints require Bearer token in Authorization header.
`
        return c.Markdown(schema)
    }

    // Regular clients get JSON
    return c.JSON(200, getOpenAPISchema())
})

Why Markdown for AI?

  • ✅ Better semantic understanding than JSON
  • ✅ Preserves structure (headers, lists, code blocks)
  • ✅ More context for LLMs to understand API behavior
  • ✅ Human-readable for debugging
Comparison with Other Routers
Feature FURSY Gin Echo Fiber
RFC 9110 Compliance ✅ Full 🔧 Partial 🔧 Partial 🔧 Partial
Automatic Negotiation Negotiate() ❌ Manual 🔧 c.Format() ❌ Manual
Quality Values (q) ✅ Automatic ❌ No ❌ No ❌ No
Accept Helpers Accepts(), AcceptsAny() ❌ No ❌ No c.Accepts()
Markdown Support ✅ Built-in ❌ Manual ❌ Manual ❌ Manual
AI Agent Ready ✅ Yes ❌ No ❌ No ❌ No

FURSY advantage: Only router with full RFC 9110 compliance, automatic q-value handling, and built-in AI agent support.

Learn more: See RFC 9110 - HTTP Semantics (Content Negotiation) for the complete specification.


📊 Observability

FURSY provides production-ready observability through the OpenTelemetry plugin, giving you complete visibility into your HTTP services with distributed tracing and metrics.

Why Observability Matters

Modern distributed systems require:

  • Distributed Tracing → Track requests across microservices
  • Performance Metrics → Monitor latency, throughput, errors
  • Error Tracking → Automatic error recording and status tracking
  • Production Debugging → Understand behavior in real-time

FURSY's OpenTelemetry plugin provides all of this with zero boilerplate - just add middleware.

Distributed Tracing

Track every request with W3C Trace Context propagation:

import (
    "context"
    "github.com/coregx/fursy"
    "github.com/coregx/fursy/plugins/opentelemetry"
    "go.opentelemetry.io/otel"
    "go.opentelemetry.io/otel/exporters/jaeger"
    sdktrace "go.opentelemetry.io/otel/sdk/trace"
)

func main() {
    // Initialize OpenTelemetry tracer
    exporter, _ := jaeger.New(jaeger.WithCollectorEndpoint(
        jaeger.WithEndpoint("http://localhost:14268/api/traces"),
    ))
    tp := sdktrace.NewTracerProvider(
        sdktrace.WithBatcher(exporter),
    )
    otel.SetTracerProvider(tp)
    defer tp.Shutdown(context.Background())

    // Add tracing middleware - that's it!
    router := fursy.New()
    router.Use(opentelemetry.Middleware("my-service"))

    router.GET("/users/:id", func(c *fursy.Context) error {
        // Automatically traced! Span includes:
        // - HTTP method, path, status
        // - Request/response headers
        // - Duration
        // - Errors (if any)
        user := getUser(c.Param("id"))
        return c.OK(user)
    })

    http.ListenAndServe(":8080", router)
}

Features:

  • W3C Trace Context - Automatic propagation across services
  • HTTP Semantic Conventions - Full OpenTelemetry compliance
  • Error Recording - Automatic error and status tracking
  • Zero Overhead Filtering - Skip health checks and metrics endpoints
Metrics Collection

Track HTTP performance with Prometheus-compatible metrics:

import (
    "github.com/coregx/fursy"
    "github.com/coregx/fursy/plugins/opentelemetry"
    "go.opentelemetry.io/otel"
    "go.opentelemetry.io/otel/exporters/prometheus"
    sdkmetric "go.opentelemetry.io/otel/sdk/metric"
)

func main() {
    // Initialize Prometheus exporter
    exporter, _ := prometheus.New()
    mp := sdkmetric.NewMeterProvider(sdkmetric.WithReader(exporter))
    otel.SetMeterProvider(mp)

    // Add metrics middleware
    router := fursy.New()
    router.Use(opentelemetry.Metrics("my-service"))

    // Metrics automatically collected:
    // - http.server.request.duration (histogram)
    // - http.server.request.count (counter)
    // - http.server.request.size (histogram)
    // - http.server.response.size (histogram)

    router.GET("/users", func(c *fursy.Context) error {
        users := getAllUsers()
        return c.OK(users)
    })

    // Expose metrics at /metrics
    router.GET("/metrics", promhttp.Handler())

    http.ListenAndServe(":8080", router)
}

Available Metrics:

Metric Type Description Labels
http.server.request.duration Histogram Request latency method, status, server
http.server.request.count Counter Total requests method, status
http.server.request.size Histogram Request body size method
http.server.response.size Histogram Response body size method, status

Cardinality Management: All metrics use low-cardinality labels (method, status, server) to prevent metrics explosion.

Custom Spans for Business Logic

Add custom spans to trace specific operations:

import "go.opentelemetry.io/otel"

router.GET("/users/:id", func(c *fursy.Context) error {
    // HTTP request span is created automatically by middleware

    // Add custom span for database query
    ctx := c.Request.Context()
    tracer := otel.Tracer("my-service")
    ctx, span := tracer.Start(ctx, "database.get_user")
    defer span.End()

    user := db.GetUser(ctx, c.Param("id"))

    // Add custom span for external API call
    _, apiSpan := tracer.Start(ctx, "api.enrich_user_data")
    enrichedData := api.Enrich(user)
    apiSpan.End()

    return c.OK(enrichedData)
})
Jaeger Integration Example

Complete setup with Jaeger for local development:

# Start Jaeger all-in-one (includes UI)
docker run -d --name jaeger \
  -e COLLECTOR_OTLP_ENABLED=true \
  -p 16686:16686 \
  -p 14268:14268 \
  jaegertracing/all-in-one:latest

# View traces at http://localhost:16686

Your fursy application will automatically send traces to Jaeger. No configuration changes needed!

Comparison with Other Routers
Feature FURSY Gin Echo Fiber
OpenTelemetry Built-in ✅ Plugin 🔧 Third-party 🔧 Third-party 🔧 Third-party
HTTP Semantic Conventions ✅ Full 🔧 Partial 🔧 Partial 🔧 Partial
Metrics API ✅ OpenTelemetry 🔧 Prometheus only 🔧 Prometheus only ✅ Built-in
Distributed Tracing ✅ W3C Trace Context 🔧 Manual 🔧 Manual 🔧 Manual
Cardinality Management ✅ Automatic ❌ Manual ❌ Manual ✅ Automatic
Zero-config ✅ One line ❌ Multiple steps ❌ Multiple steps ✅ One line

FURSY advantage: Official OpenTelemetry plugin with full HTTP semantic conventions compliance and zero-config setup.

Learn more: See OpenTelemetry Plugin Documentation for advanced configuration, custom spans, and production patterns.


📖 Documentation

Status: 🟡 In Development


🎯 Comparison

Feature FURSY Gin Echo Chi Fiber
Type-Safe Handlers
Auto Validation 🔧 Manual 🔧 Manual 🔧 Manual 🔧 Manual
Content Negotiation ✅ RFC 9110 🔧 Partial 🔧 Partial 🔧 Partial
Zero Deps (core)
OpenAPI Built-in 🔧 Plugin 🔧 Plugin 🔧 Plugin 🔧 Plugin
RFC 9457 Errors
Performance ⭐⭐⭐⭐⭐ ⭐⭐⭐⭐ ⭐⭐⭐⭐ ⭐⭐⭐ ⭐⭐⭐⭐⭐
Go Version 1.25+ 1.13+ 1.17+ 1.16+ 1.17+

FURSY is unique: Only router combining furious performance, type-safe generics, automatic validation, RFC 9110 content negotiation, OpenAPI, and RFC 9457 with minimal dependencies.


📈 Status

Current Version: v0.3.0 (Production Ready)

Status: Production Ready - Complete ecosystem with real-time, database, and production examples

Coverage: 93.1% test coverage (core), 650+ tests total

Performance: 256 ns/op (static), 326 ns/op (parametric), 1 alloc/op

Roadmap:

✅ v0.1.0          ✅ v0.2.0          ✅ v0.3.0             🎯 v1.0.0 LTS
(Foundation)     (Docs+Examples)  (Real-time+DB)         (TBD - After Full
                                                          API Stabilization)
    │                  │                 │                       │
    ▼                  ▼                 ▼                       ▼
Core Router        Documentation    Real-Time+DB            Stable API
Middleware         11 Examples      Production Ready        Long-Term Support
Production         Validation       2 Plugins               (NOT Rushing!)
Features           OpenAPI          DDD Boilerplate

Current Status: v0.3.0 Production Ready ✅ Ecosystem: stream v0.1.0 (SSE + WebSocket), 2 production plugins, 10 examples Next: v0.x.x feature releases as needed (Cache, more plugins, community tools) v1.0.0 LTS: After 6-12 months of production usage and full API stabilization


🤝 Contributing

We welcome contributions! Please see:

Development Requirements:

  • Go 1.25+
  • golangci-lint
  • Follow git-flow branching model

Want to help?

  • ⭐ Star the repo
  • 📢 Share with others
  • 🐛 Report bugs or request features
  • 💬 Join discussions (coming soon)

📜 License

MIT License - see LICENSE file for details



💡 Inspiration

FURSY stands on the shoulders of giants:

Technical:

Philosophy:

  • Relica - Zero deps, type safety, quality
Special Thanks

Professor Ancha Baranova - This project would not have been possible without her invaluable help and support. Her assistance was crucial in making all coregx projects a reality.


📞 Contact

Questions? Check back soon for:

  • GitHub Discussions
  • Discord server
  • Documentation site

🔥 FURSY - Unleash Routing Fursy

Blazing FastMinimal DependenciesType-SafeFurious


Built with ❤️ by the coregx team

Version: v0.3.0 - Production Ready Ecosystem: stream v0.1.0 + 2 plugins + 10 examples + DDD boilerplate Next: v1.0.0 LTS (after full API stabilization)


Documentation

Overview

Package fursy provides Context types for HTTP request handling.

Package fursy provides the Empty type for type-safe handlers without request/response bodies.

Package fursy provides common HTTP errors for the FURSY router.

Package fursy provides a high-performance HTTP router for Go 1.25+.

FURSY combines type-safe routing with modern Go features like generics, providing fast URL matching (<100ns), zero dependencies, and clean API.

Quick Start

router := fursy.New()

router.GET("/users/:id", func(c *fursy.Box) error {
	id := c.Param("id")
	return c.String(200, "User ID: "+id)
})

http.ListenAndServe(":8080", router)

Route Types

FURSY supports three types of routes:

  • Static: /users
  • Parameters: /users/:id
  • Wildcards: /files/*path

Performance

FURSY uses a radix tree for routing, providing <100ns lookups and zero allocations for simple routes.

HTTP Methods

All standard HTTP methods are supported: GET, POST, PUT, DELETE, PATCH, HEAD, OPTIONS.

URL Parameters

Extract parameters using Box methods:

id := c.Param("id")
page := c.Query("page")
username := c.Form("username")

Error Handling

  • 404 Not Found: Automatic for unregistered routes
  • 405 Method Not Allowed: Automatic when route exists but method differs

See Router documentation for more details.

Index

Constants

View Source
const (
	MIMEApplicationJSON  = "application/json"
	MIMETextHTML         = "text/html"
	MIMEApplicationXML   = "application/xml"
	MIMETextXML          = "text/xml"
	MIMETextPlain        = "text/plain"
	MIMETextMarkdown     = "text/markdown" // Added for AI agents and documentation
	MIMEApplicationForm  = "application/x-www-form-urlencoded"
	MIMEMultipartForm    = "multipart/form-data"
	MIMEApplicationXYAML = "application/x-yaml"
	MIMEApplicationYAML  = "application/yaml"
	MIMEApplicationTOML  = "application/toml"
)

MIME type constants for common content types.

Variables

View Source
var (
	// ErrUnauthorized is returned when authentication fails.
	ErrUnauthorized = errors.New("unauthorized")

	// ErrForbidden is returned when user is authenticated but not authorized.
	ErrForbidden = errors.New("forbidden")

	// ErrNotFound is returned when a resource is not found.
	ErrNotFound = errors.New("not found")

	// ErrBadRequest is returned when the request is invalid.
	ErrBadRequest = errors.New("bad request")

	// ErrInternalServerError is returned for server errors.
	ErrInternalServerError = errors.New("internal server error")
)

Common HTTP errors.

View Source
var ErrInvalidRedirectCode = errors.New("fursy: invalid redirect code (must be 3xx)")

ErrInvalidRedirectCode is returned when redirect code is not 3xx.

View Source
var ErrStreamNotImported = errors.New("fursy: stream plugin not imported - add 'import _ \"github.com/coregx/fursy/plugins/stream\"' to your code")

ErrStreamNotImported is returned when SSE or WebSocket methods are called without importing github.com/coregx/fursy/plugins/stream package.

Functions

func DELETE

func DELETE[Req, Res any](r *Router, path string, handler Handler[Req, Res])

DELETE registers a type-safe handler for DELETE requests to the specified path.

The handler will receive a Box[Req, Res] with automatically bound request body.

Example:

fursy.DELETE[fursy.Empty, fursy.Empty](router, "/users/:id", func(c *fursy.Box[fursy.Empty, fursy.Empty]) error {
    id := c.Param("id")
    db.DeleteUser(id)
    return c.NoContent(204)
})

func GET

func GET[Req, Res any](r *Router, path string, handler Handler[Req, Res])

GET registers a type-safe handler for GET requests to the specified path.

Since Go doesn't support generic methods, we use top-level functions instead. This provides clean type-safe routing with automatic request binding.

Type parameters:

  • Req: The expected request body type (use Empty for GET requests)
  • Res: The response body type

Example:

type UserResponse struct {
    ID   int    `json:"id"`
    Name string `json:"name"`
}

fursy.GET[fursy.Empty, UserResponse](router, "/users/:id", func(c *fursy.Box[fursy.Empty, UserResponse]) error {
    id := c.Param("id")
    user := db.GetUser(id)
    return c.OK(UserResponse{ID: user.ID, Name: user.Name})
})
func HEAD[Req, Res any](r *Router, path string, handler Handler[Req, Res])

HEAD registers a type-safe handler for HEAD requests to the specified path.

The handler will receive a Box[Req, Res] with automatically bound request body. HEAD requests should not return a body, only headers.

Example:

fursy.HEAD[fursy.Empty, fursy.Empty](router, "/users/:id", func(c *fursy.Box[fursy.Empty, fursy.Empty]) error {
    id := c.Param("id")
    if db.UserExists(id) {
        return c.NoContent(200)
    }
    return c.NoContent(404)
})

func OPTIONS

func OPTIONS[Req, Res any](r *Router, path string, handler Handler[Req, Res])

OPTIONS registers a type-safe handler for OPTIONS requests to the specified path.

The handler will receive a Box[Req, Res] with automatically bound request body. OPTIONS requests are typically used for CORS preflight checks.

Example:

fursy.OPTIONS[fursy.Empty, fursy.Empty](router, "/users", func(c *fursy.Box[fursy.Empty, fursy.Empty]) error {
    c.SetHeader("Allow", "GET, POST, PUT, DELETE")
    c.SetHeader("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE")
    return c.NoContent(200)
})

func PATCH

func PATCH[Req, Res any](r *Router, path string, handler Handler[Req, Res])

PATCH registers a type-safe handler for PATCH requests to the specified path.

The handler will receive a Box[Req, Res] with automatically bound request body.

Example:

type PatchUserRequest struct {
    Name  *string `json:"name,omitempty"`
    Email *string `json:"email,omitempty"`
}

fursy.PATCH[PatchUserRequest, UserResponse](router, "/users/:id", func(c *fursy.Box[PatchUserRequest, UserResponse]) error {
    id := c.Param("id")
    req := c.ReqBody
    user := db.PatchUser(id, req)
    return c.OK(UserResponse{ID: user.ID, Name: user.Name})
})

func POST

func POST[Req, Res any](r *Router, path string, handler Handler[Req, Res])

POST registers a type-safe handler for POST requests to the specified path.

The handler will receive a Box[Req, Res] with automatically bound request body.

Example:

type CreateUserRequest struct {
    Name  string `json:"name" validate:"required"`
    Email string `json:"email" validate:"required,email"`
}

type UserResponse struct {
    ID   int    `json:"id"`
    Name string `json:"name"`
}

fursy.POST[CreateUserRequest, UserResponse](router, "/users", func(c *fursy.Box[CreateUserRequest, UserResponse]) error {
    req := c.ReqBody
    user := db.CreateUser(req.Name, req.Email)
    return c.Created("/users/"+user.ID, UserResponse{ID: user.ID, Name: user.Name})
})

func PUT

func PUT[Req, Res any](r *Router, path string, handler Handler[Req, Res])

PUT registers a type-safe handler for PUT requests to the specified path.

The handler will receive a Box[Req, Res] with automatically bound request body.

Example:

type UpdateUserRequest struct {
    Name  string `json:"name"`
    Email string `json:"email"`
}

fursy.PUT[UpdateUserRequest, UserResponse](router, "/users/:id", func(c *fursy.Box[UpdateUserRequest, UserResponse]) error {
    id := c.Param("id")
    req := c.ReqBody
    user := db.UpdateUser(id, req.Name, req.Email)
    return c.OK(UserResponse{ID: user.ID, Name: user.Name})
})

Types

type Box

type Box[Req, Res any] struct {
	*Context

	// ReqBody is the parsed and validated request body.
	// It is automatically bound from the request based on Content-Type.
	// For handlers with no request body, use Empty type and ReqBody will be nil.
	ReqBody *Req

	// ResBody is the response body to be sent.
	// Set this field or use type-safe response methods (OK, Created, etc.)
	// For handlers with no response body, use Empty type.
	ResBody *Res
}

Box is a type-safe context for HTTP request handling with request/response types.

It embeds *Context, providing all base functionality (params, middleware, etc.), while adding type-safe request and response body handling.

Type parameters:

  • Req: The expected request body type (use Empty if no body)
  • Res: The response body type (use Empty if no structured response)

The context automatically binds the request body to ReqBody based on Content-Type. The response body (ResBody) can be set and sent using type-safe methods like OK(), Created(), etc.

Example:

type CreateUserRequest struct {
    Name  string `json:"name" validate:"required"`
    Email string `json:"email" validate:"required,email"`
}

type UserResponse struct {
    ID    int    `json:"id"`
    Name  string `json:"name"`
    Email string `json:"email"`
}

router.POST[CreateUserRequest, UserResponse]("/users", func(c *Box[CreateUserRequest, UserResponse]) error {
    // ReqBody is automatically bound from JSON
    req := c.ReqBody

    // Create user
    user := db.CreateUser(req.Name, req.Email)

    // Set response
    return c.Created("/users/"+user.ID, UserResponse{
        ID:    user.ID,
        Name:  user.Name,
        Email: user.Email,
    })
})

func (*Box[Req, Res]) Accepted

func (c *Box[Req, Res]) Accepted(data Res) error

Accepted sends a 202 Accepted response with data.

Use this when a request has been accepted for processing but the processing has not been completed yet (e.g., async operations).

Example:

return c.Accepted(TaskResponse{TaskID: "abc123", Status: "pending"})

func (*Box[Req, Res]) BadRequest

func (c *Box[Req, Res]) BadRequest(data Res) error

BadRequest sends a 400 Bad Request response with error details.

Example:

return c.BadRequest(ErrorResponse{Message: "Invalid email format"})

func (*Box[Req, Res]) Bind

func (c *Box[Req, Res]) Bind() error

Bind binds the request body to ReqBody based on Content-Type.

Supported content types:

  • application/json (default)
  • application/xml, text/xml
  • application/x-www-form-urlencoded
  • multipart/form-data

If a validator is set via Router.SetValidator(), the request body will be automatically validated after binding. Validation errors are returned as ValidationErrors.

This method is automatically called by the generic handler adapter, so you typically don't need to call it manually.

Returns error if binding or validation fails.

Example:

// Manual binding (usually automatic)
if err := c.Bind(); err != nil {
    return c.BadRequest(ErrorResponse{Message: err.Error()})
}

func (*Box[Req, Res]) Created

func (c *Box[Req, Res]) Created(location string, data Res) error

Created sends a 201 Created response with Location header and data.

The location parameter should be the URL of the newly created resource.

Example:

return c.Created("/users/123", UserResponse{ID: 123, Name: "John"})

func (*Box[Req, Res]) Forbidden

func (c *Box[Req, Res]) Forbidden(data Res) error

Forbidden sends a 403 Forbidden response with error details.

Example:

return c.Forbidden(ErrorResponse{Message: "Insufficient permissions"})

func (*Box[Req, Res]) InternalServerError

func (c *Box[Req, Res]) InternalServerError(data Res) error

InternalServerError sends a 500 Internal Server Error response with error details.

Example:

return c.InternalServerError(ErrorResponse{Message: "Database connection failed"})

func (*Box[Req, Res]) NoContentSuccess

func (c *Box[Req, Res]) NoContentSuccess() error

NoContentSuccess sends a 204 No Content response. This is a convenience method for successful operations with no response body.

Common for DELETE operations and some PUT/PATCH updates. REST best practice: DELETE should return 204, not 200.

Example:

router.DELETE[DeleteUserRequest, Empty]("/users/:id", func(c *Box[DeleteUserRequest, Empty]) error {
    deleteUser(c.ReqBody.ID)
    return c.NoContentSuccess()  // 204 No Content
})

func (*Box[Req, Res]) NotFound

func (c *Box[Req, Res]) NotFound(data Res) error

NotFound sends a 404 Not Found response with error details.

Example:

return c.NotFound(ErrorResponse{Message: "User not found"})

func (*Box[Req, Res]) OK

func (c *Box[Req, Res]) OK(data Res) error

OK sends a 200 OK response with the given data.

Example:

return c.OK(UserResponse{ID: 1, Name: "John"})

func (*Box[Req, Res]) Unauthorized

func (c *Box[Req, Res]) Unauthorized(data Res) error

Unauthorized sends a 401 Unauthorized response with error details.

Example:

return c.Unauthorized(ErrorResponse{Message: "Invalid credentials"})

func (*Box[Req, Res]) UpdatedNoContent

func (c *Box[Req, Res]) UpdatedNoContent() error

UpdatedNoContent sends a 204 No Content response for successful updates without response body. Use this when PUT/PATCH operations succeed but don't return data.

Example:

router.PUT[UpdateUserRequest, Empty]("/users/:id", func(c *Box[UpdateUserRequest, Empty]) error {
    updateUser(c.ReqBody)
    return c.UpdatedNoContent()  // 204 No Content
})

func (*Box[Req, Res]) UpdatedOK

func (c *Box[Req, Res]) UpdatedOK(data Res) error

UpdatedOK sends a 200 OK response for successful updates with response body. This is an alias for OK() but provides semantic clarity for PUT/PATCH operations.

Example:

router.PUT[UpdateUserRequest, UserResponse]("/users/:id", func(c *Box[UpdateUserRequest, UserResponse]) error {
    updated := updateUser(c.ReqBody)
    return c.UpdatedOK(updated)  // 200 OK - semantically clear it's an update
})

type Components

type Components struct {
	// Schemas is a map of reusable Schema objects.
	Schemas map[string]*Schema `json:"schemas,omitempty"`

	// Responses is a map of reusable Response objects.
	Responses map[string]Response `json:"responses,omitempty"`

	// Parameters is a map of reusable Parameter objects.
	Parameters map[string]Parameter `json:"parameters,omitempty"`

	// RequestBodies is a map of reusable RequestBody objects.
	RequestBodies map[string]RequestBody `json:"requestBodies,omitempty"`

	// Headers is a map of reusable Header objects.
	Headers map[string]Header `json:"headers,omitempty"`

	// SecuritySchemes is a map of reusable SecurityScheme objects.
	SecuritySchemes map[string]SecurityScheme `json:"securitySchemes,omitempty"`
}

Components holds reusable objects.

type Contact

type Contact struct {
	Name  string `json:"name,omitempty"`
	URL   string `json:"url,omitempty"`
	Email string `json:"email,omitempty"`
}

Contact information for the exposed API.

type Context

type Context struct {
	// Request is the current HTTP request.
	Request *http.Request

	// Response is the response writer.
	Response http.ResponseWriter
	// contains filtered or unexported fields
}

Context is the base context for all handlers and middleware. It provides access to request/response, routing info, and middleware chain execution.

Context is designed to be embedded in higher-level context types (like generic Box[Req, Res]) while providing all the core functionality needed for middleware and handlers.

The context is pooled and reused across requests for zero allocations. Do not store Context references - all operations must complete within the handler's execution.

func (*Context) APIVersion

func (c *Context) APIVersion() Version

APIVersion returns the current API version from the request.

Version is extracted in this order:

  1. Api-Version header
  2. URL path (/v1/, /v2/, etc.)

Returns zero Version if no version found.

Example:

version := c.APIVersion()
if version.Major == 1 {
    // Handle v1 request
}

func (*Context) Abort

func (c *Context) Abort()

Abort prevents pending handlers from being called. Note that this does not stop the current handler - it only prevents subsequent handlers in the chain.

This is useful when a middleware wants to stop the chain (e.g., authentication failure).

Example:

func RequireAuth() HandlerFunc {
    return func(c *Context) error {
        if !isAuthenticated(c) {
            c.Abort()
            return c.JSON(401, map[string]string{"error": "unauthorized"})
        }
        return c.Next()
    }
}

func (*Context) Accepted

func (c *Context) Accepted(obj any) error

Accepted sends a 202 Accepted JSON response. Use this when the request has been accepted for processing but not completed.

Common for async operations, background jobs, or queued tasks.

Example:

router.POST("/jobs", func(c *fursy.Context) error {
    jobID := startAsyncJob(c)
    return c.Accepted(map[string]string{"jobId": jobID})  // 202 Accepted
})

func (*Context) Accepts added in v0.2.0

func (c *Context) Accepts(mediaType string) bool

Accepts returns true if the specified media type is acceptable based on the request's Accept header.

This is a convenience wrapper around NegotiateFormat for simple cases where you want to check if a specific media type is acceptable.

Example:

if c.Accepts(MIMETextMarkdown) {
    return c.Markdown(renderMarkdown(docs))
}
return c.JSON(200, data)

func (*Context) AcceptsAny added in v0.2.0

func (c *Context) AcceptsAny(mediaTypes ...string) string

AcceptsAny returns the best matching media type from the provided options based on the request's Accept header and quality values (q-values).

This is an alias for NegotiateFormat with a more intuitive name for checking multiple media types. Returns empty string if none of the offered types are acceptable.

Example:

switch c.AcceptsAny(MIMETextMarkdown, MIMETextHTML, MIMEApplicationJSON) {
case MIMETextMarkdown:
    return c.Markdown(renderMarkdown(data))
case MIMETextHTML:
    return c.HTML(200, renderHTML(data))
default:
    return c.JSON(200, data)
}

func (*Context) Blob

func (c *Context) Blob(code int, contentType string, data []byte) error

Blob sends a binary response with custom content type. This is useful for sending raw binary data like images or files.

Example:

imageData := []byte{...}
return c.Blob(200, "image/png", imageData)

func (*Context) Created

func (c *Context) Created(obj any) error

Created sends a 201 Created JSON response. Use this for successful POST requests that create a new resource.

REST best practice: POST operations that create resources should return 201, not 200.

Example:

router.POST("/users", func(c *fursy.Context) error {
    newUser := createUser(c)
    return c.Created(newUser)  // 201 Created
})

func (*Context) DB added in v0.3.0

func (c *Context) DB() any

DB returns the database connection from the context.

Returns nil if database middleware is not configured.

Requires: github.com/coregx/fursy/plugins/database package

Example:

// In main.go:
import (
    "database/sql"
    "github.com/coregx/fursy"
    "github.com/coregx/fursy/plugins/database"
    _ "github.com/lib/pq" // PostgreSQL driver
)

sqlDB, _ := sql.Open("postgres", dsn)
db := database.NewDB(sqlDB)

router := fursy.New()
router.Use(database.Middleware(db))

router.GET("/users/:id", func(c *fursy.Context) error {
    db := c.DB()
    if db == nil {
        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"))
    }
    if err != nil {
        return c.Problem(fursy.InternalServerError(err.Error()))
    }

    return c.JSON(200, user)
})

Note: This method signature returns 'any' to avoid importing github.com/coregx/fursy/plugins/database in fursy core. The actual type is *database.DB when database middleware is configured.

func (*Context) Form

func (c *Context) Form(name string) string

Form returns the first value for the named form parameter. It checks both POST/PUT body parameters and URL query parameters. Form parameters take precedence over query parameters.

For multipart forms, it parses up to 32MB of data.

Example:

// POST /login with body: username=john&password=secret
username := c.Form("username") // "john"

func (*Context) FormDefault

func (c *Context) FormDefault(name, defaultValue string) string

FormDefault returns the form parameter value or a default value. If the parameter doesn't exist or is empty, returns defaultValue.

Example:

role := c.FormDefault("role", "user") // "user" if not provided

func (*Context) Get

func (c *Context) Get(key string) any

Get retrieves data from the context. Returns nil if the key doesn't exist.

This is useful for passing data between middleware and handlers.

Example:

// In authentication middleware:
c.Set("userID", "123")

// In handler:
userID := c.Get("userID").(string)

func (*Context) GetBool

func (c *Context) GetBool(key string) bool

GetBool retrieves a bool value from the context. Returns false if the key doesn't exist or value is not a bool.

Example:

authenticated := c.GetBool("authenticated")

func (*Context) GetHeader

func (c *Context) GetHeader(key string) string

GetHeader returns a request header value. Returns empty string if the header doesn't exist.

Example:

userAgent := c.GetHeader("User-Agent")

func (*Context) GetInt

func (c *Context) GetInt(key string) int

GetInt retrieves an int value from the context. Returns 0 if the key doesn't exist or value is not an int.

Example:

page := c.GetInt("page")

func (*Context) GetString

func (c *Context) GetString(key string) string

GetString retrieves a string value from the context. Returns empty string if the key doesn't exist or value is not a string.

Example:

userID := c.GetString("userID")

func (*Context) IsAborted

func (c *Context) IsAborted() bool

IsAborted returns true if the context was aborted. This can be used to check if a middleware called Abort().

func (*Context) JSON

func (c *Context) JSON(code int, obj any) error

JSON sends a JSON response. The obj is encoded using encoding/json and sent with application/json content type.

Example:

return c.JSON(200, map[string]string{"message": "success"})

func (*Context) JSONIndent

func (c *Context) JSONIndent(code int, obj any, indent string) error

JSONIndent sends a JSON response with indentation for pretty-printing. This is useful for debugging or human-readable responses.

Example:

return c.JSONIndent(200, data, "  ") // 2-space indent

func (*Context) Markdown added in v0.2.0

func (c *Context) Markdown(content string) error

Markdown sends a markdown text response with status 200. Sets Content-Type to "text/markdown; charset=utf-8".

This is a convenience method for serving markdown content, particularly useful for AI agents and documentation endpoints.

Example:

router.GET("/docs.md", func(c *Context) error {
    md := `# API Documentation

## Endpoints
- GET /users - List all users
- POST /users - Create new user`
    return c.Markdown(md)
})

func (*Context) Negotiate

func (c *Context) Negotiate(status int, data any) error

Negotiate performs content negotiation and sends the response in the best format.

This is a convenience method that combines NegotiateFormat with automatic response rendering. It automatically sets the Vary: Accept header for proper HTTP caching.

Supported formats:

  • application/json (JSON)
  • application/xml, text/xml (XML)
  • text/html (HTML - requires HTMLData and HTMLTemplate)
  • text/plain (Plain text)

Returns ErrNotAcceptable if no acceptable format is found.

Example:

type User struct {
    ID   int    `json:"id" xml:"id"`
    Name string `json:"name" xml:"name"`
}

user := User{ID: 1, Name: "John"}
return c.Negotiate(200, user)
// Client with "Accept: application/json" receives JSON
// Client with "Accept: application/xml" receives XML

func (*Context) NegotiateFormat

func (c *Context) NegotiateFormat(offered ...string) string

NegotiateFormat returns the best offered content type based on the Accept header.

This method performs RFC 9110 compliant content negotiation, including:

  • Quality value (q) weighting
  • Specificity matching (explicit > wildcard)
  • Parameter precedence

Returns the selected content type, or an empty string if no match found.

Example:

format := c.NegotiateFormat(fursy.MIMEApplicationJSON, fursy.MIMETextHTML, fursy.MIMEApplicationXML)
switch format {
case fursy.MIMEApplicationJSON:
    return c.JSON(200, data)
case fursy.MIMETextHTML:
    return c.HTML(200, "template", data)
case fursy.MIMEApplicationXML:
    return c.XML(200, data)
default:
    return c.Problem(NotAcceptable("No acceptable format found"))
}

func (*Context) Next

func (c *Context) Next() error

Next executes the next handler in the middleware chain. It returns the error from the handler, allowing middleware to handle or transform errors.

Example middleware:

func Logger() HandlerFunc {
    return func(c *Context) error {
        start := time.Now()
        err := c.Next()  // Call next handler
        log.Printf("%s - %v", c.Request.URL.Path, time.Since(start))
        return err
    }
}

func (*Context) NoContent

func (c *Context) NoContent(code int) error

NoContent sends a response with no body. This is commonly used for 204 No Content responses.

Example:

return c.NoContent(204) // Successful deletion

func (*Context) NoContentSuccess

func (c *Context) NoContentSuccess() error

NoContentSuccess sends a 204 No Content response. This is a convenience method for successful operations with no response body.

Common for DELETE operations and some PUT/PATCH updates. REST best practice: DELETE should return 204, not 200.

Example:

router.DELETE("/users/:id", func(c *fursy.Context) error {
    deleteUser(c.Param("id"))
    return c.NoContentSuccess()  // 204 No Content
})

func (*Context) OK

func (c *Context) OK(obj any) error

OK sends a 200 OK JSON response. This is a convenience method for the most common success case.

Use this for successful GET requests or operations that return data.

Example:

router.GET("/users", func(c *fursy.Context) error {
    users := getAllUsers()
    return c.OK(users)  // 200 OK
})

func (*Context) Param

func (c *Context) Param(name string) string

Param returns the value of the URL parameter by name. Returns empty string if the parameter doesn't exist.

Example:

// Route: /users/:id
// Request: /users/123
id := c.Param("id") // "123"

func (*Context) PostForm

func (c *Context) PostForm(name string) string

PostForm returns the form value from POST/PUT body only (not URL query). Unlike Form(), this does not fall back to query parameters.

Example:

// POST /update?id=123 with body: name=john
name := c.PostForm("name") // "john"
id := c.PostForm("id")     // "" (not in POST body)

func (*Context) Problem

func (c *Context) Problem(p Problem) error

Problem sends an RFC 9457 Problem Details response.

Problem Details (RFC 9457) provides a standard way to carry machine-readable details of errors in HTTP responses, with Content-Type: application/problem+json.

Example:

return c.Problem(fursy.NotFound("User not found"))

return c.Problem(fursy.UnprocessableEntity("Invalid input").
    WithExtension("field", "email").
    WithExtension("reason", "already exists"))

For validation errors, use ValidationProblem:

if err := c.Bind(); err != nil {
    if verr, ok := err.(ValidationErrors); ok {
        return c.Problem(ValidationProblem(verr))
    }
    return c.Problem(BadRequest(err.Error()))
}

func (*Context) Query

func (c *Context) Query(name string) string

Query returns the first value for the named query parameter. Returns empty string if the parameter doesn't exist.

The query string is parsed lazily on first access and cached for subsequent calls.

Example:

// Request: /users?page=2&limit=10
page := c.Query("page")   // "2"
limit := c.Query("limit") // "10"

func (*Context) QueryDefault

func (c *Context) QueryDefault(name, defaultValue string) string

QueryDefault returns the query parameter value or a default value. If the parameter doesn't exist or is empty, returns defaultValue.

Example:

// Request: /users?page=2
page := c.QueryDefault("page", "1")   // "2"
limit := c.QueryDefault("limit", "10") // "10" (default)

func (*Context) QueryValues

func (c *Context) QueryValues(name string) []string

QueryValues returns all values for the named query parameter. Returns nil if the parameter doesn't exist.

Example:

// Request: /search?tag=go&tag=web&tag=api
tags := c.QueryValues("tag") // []string{"go", "web", "api"}

func (*Context) Redirect

func (c *Context) Redirect(code int, url string) error

Redirect sends an HTTP redirect response. The code must be in the 3xx range (300-308).

Common redirect codes:

  • 301: Moved Permanently
  • 302: Found (temporary redirect)
  • 303: See Other
  • 307: Temporary Redirect (preserves method)
  • 308: Permanent Redirect (preserves method)

Example:

return c.Redirect(302, "/login")

func (*Context) Router

func (c *Context) Router() *Router

Router returns the router instance that is handling this request. This can be used to access router configuration or state.

func (*Context) SSE added in v0.3.0

func (c *Context) SSE(handler func(conn any) error) error

SSE upgrades the HTTP connection to Server-Sent Events.

The handler function receives an SSE connection and should handle the SSE lifecycle (register to hub, send events, etc.).

The connection is automatically closed when the handler returns.

Requires: github.com/coregx/stream/sse package

Example:

// In main.go:
import (
    "github.com/coregx/fursy"
    "github.com/coregx/fursy/plugins/stream"
    "github.com/coregx/stream/sse"
)

hub := sse.NewHub[Notification]()
go hub.Run()
defer hub.Close()

router := fursy.New()
router.Use(stream.SSEHub(hub))

router.GET("/events", func(c *fursy.Context) error {
    hub, _ := stream.GetSSEHub[Notification](c)

    return c.SSE(func(conn *sse.Conn) error {
        hub.Register(conn)
        defer hub.Unregister(conn)
        <-conn.Done()
        return nil
    })
})

Note: This method signature is defined in fursy core, but requires github.com/coregx/stream/sse to be imported in your code for the Conn type.

func (*Context) Set

func (c *Context) Set(key string, value any)

Set stores data in the context. This is useful for passing data between middleware and handlers.

Example:

c.Set("userID", "123")
c.Set("authenticated", true)

func (*Context) SetHeader

func (c *Context) SetHeader(key, value string)

SetHeader sets a response header. This must be called before writing the response body.

Example:

c.SetHeader("X-Request-ID", requestID)
return c.String(200, "OK")

func (*Context) Stream

func (c *Context) Stream(code int, contentType string, r io.Reader) error

Stream sends a response from an io.Reader. This is useful for streaming large files or data without loading everything into memory.

Example:

file, _ := os.Open("large-file.pdf")
defer file.Close()
return c.Stream(200, "application/pdf", file)

func (*Context) String

func (c *Context) String(code int, s string) error

String sends a plain text response.

Example:

return c.String(200, "Hello, World!")

func (*Context) Text

func (c *Context) Text(s string) error

Text sends a 200 OK plain text response. This is a convenience method for simple text responses.

Example:

router.GET("/ping", func(c *fursy.Context) error {
    return c.Text("pong")  // 200 OK, text/plain
})

func (*Context) WebSocket added in v0.3.0

func (c *Context) WebSocket(handler func(conn any) error, opts any) error

WebSocket upgrades the HTTP connection to WebSocket.

The handler function receives a WebSocket connection and should handle the WebSocket lifecycle (register to hub, read/write messages, etc.).

The connection is automatically closed when the handler returns.

Requires: github.com/coregx/stream/websocket package

Example:

// In main.go:
import (
    "github.com/coregx/fursy"
    "github.com/coregx/fursy/plugins/stream"
    "github.com/coregx/stream/websocket"
)

hub := websocket.NewHub()
go hub.Run()
defer hub.Close()

router := fursy.New()
router.Use(stream.WebSocketHub(hub))

router.GET("/ws", func(c *fursy.Context) error {
    hub, _ := stream.GetWebSocketHub(c)

    return c.WebSocket(func(conn *websocket.Conn) error {
        hub.Register(conn)
        defer hub.Unregister(conn)

        for {
            msgType, data, err := conn.Read()
            if err != nil {
                return err
            }
            hub.Broadcast(data)
        }
    }, nil)
})

Note: This method signature is defined in fursy core, but requires github.com/coregx/stream/websocket to be imported in your code for the Conn type.

func (*Context) XML

func (c *Context) XML(code int, obj any) error

XML sends an XML response. The obj is encoded using encoding/xml and sent with application/xml content type.

Example:

type User struct {
	XMLName xml.Name `xml:"user"`
	ID      string   `xml:"id"`
	Name    string   `xml:"name"`
}
return c.XML(200, User{ID: "123", Name: "John"})

type DeprecationInfo

type DeprecationInfo struct {
	// Version that is deprecated.
	Version Version

	// SunsetDate is when this version will be removed (RFC 8594).
	SunsetDate *time.Time

	// Message is a human-readable deprecation message.
	Message string

	// Link to migration guide or new version docs.
	Link string
}

DeprecationInfo contains information about API deprecation.

RFC 8594 defines the Sunset HTTP header to communicate deprecation.

func (*DeprecationInfo) SetDeprecationHeaders

func (d *DeprecationInfo) SetDeprecationHeaders(c *Context)

SetDeprecationHeaders sets deprecation headers on the response.

RFC 8594 Sunset Header:

Sunset: Sat, 31 Dec 2025 23:59:59 GMT

Also sets:

  • Deprecation: true (draft standard)
  • Link: <url>; rel="sunset" (RFC 8594)
  • Warning: "299 - \"message\"" (RFC 7234)

type Empty

type Empty struct{}

Empty represents the absence of a request or response body in type-safe handlers.

Use Empty when a handler doesn't need to bind a request body or send a response body. This maintains type safety while indicating "no data" intent.

Examples:

// Handler with no request body, returns string
router.GET[Empty, string]("/hello", func(c *Box[Empty, string]) error {
    c.ResBody = new(string)
    *c.ResBody = "Hello, World!"
    return c.OK(*c.ResBody)
})

// Handler with request body, no response body
router.POST[CreateUserRequest, Empty]("/users", func(c *Box[CreateUserRequest, Empty]) error {
    user := c.ReqBody
    db.CreateUser(user)
    return c.NoContent(201)
})

// Handler with neither request nor response body
router.DELETE[Empty, Empty]("/cache", func(c *Box[Empty, Empty]) error {
    cache.Clear()
    return c.NoContent(204)
})

type Example

type Example struct {
	Summary       string `json:"summary,omitempty"`
	Description   string `json:"description,omitempty"`
	Value         any    `json:"value,omitempty"`
	ExternalValue string `json:"externalValue,omitempty"`
}

Example represents an example value.

type Handler

type Handler[Req, Res any] func(*Box[Req, Res]) error

Handler is a type-safe handler function for HTTP requests with typed request/response bodies.

Type parameters:

  • Req: The expected request body type (use Empty if no body)
  • Res: The response body type (use Empty if no structured response)

The handler receives a Box[Req, Res] with automatically bound request body (ReqBody) and provides type-safe methods for sending responses.

Example:

type CreateUserRequest struct {
    Name  string `json:"name"`
    Email string `json:"email"`
}

type UserResponse struct {
    ID   int    `json:"id"`
    Name string `json:"name"`
}

func createUser(c *Box[CreateUserRequest, UserResponse]) error {
    req := c.ReqBody  // Type: *CreateUserRequest
    user := db.Create(req.Name, req.Email)
    return c.Created("/users/"+user.ID, UserResponse{
        ID:   user.ID,
        Name: user.Name,
    })
}

router.POST[CreateUserRequest, UserResponse]("/users", createUser)

type HandlerFunc

type HandlerFunc func(*Context) error

HandlerFunc is the function signature for simple non-generic handlers and middleware. It receives a Context and returns an error.

Example handler:

func GetUser(c *Context) error {
    id := c.Param("id")
    return c.JSON(200, map[string]string{"id": id})
}

Example middleware:

func Logger() HandlerFunc {
    return func(c *Context) error {
        start := time.Now()
        err := c.Next() // Call next handler
        log.Printf("%s %s - %v", c.Request.Method, c.Request.URL.Path, time.Since(start))
        return err
    }
}

For type-safe generic handlers with automatic request/response binding, use Handler[Req, Res] instead, which receives Box[Req, Res].

func DeprecateVersion

func DeprecateVersion(info DeprecationInfo) HandlerFunc

DeprecateVersion is a middleware that marks a version as deprecated.

Adds deprecation headers to all responses for this version.

Example:

v1 := router.Group("/api/v1")
v1.Use(fursy.DeprecateVersion(fursy.DeprecationInfo{
    Version: fursy.Version{Major: 1},
    SunsetDate: &sunsetDate,
    Message: "Please migrate to v2",
    Link: "https://api.example.com/docs/v2-migration",
}))

func RequireVersion

func RequireVersion(required Version) HandlerFunc

RequireVersion is a middleware that requires a specific API version.

If the request version doesn't match, returns 404 Not Found or 400 Bad Request.

Example:

v1 := router.Group("/api/v1")
v1.Use(fursy.RequireVersion(fursy.Version{Major: 1}))

v2 := router.Group("/api/v2")
v2.Use(fursy.RequireVersion(fursy.Version{Major: 2}))
type Header struct {
	Description string  `json:"description,omitempty"`
	Required    bool    `json:"required,omitempty"`
	Deprecated  bool    `json:"deprecated,omitempty"`
	Schema      *Schema `json:"schema,omitempty"`
}

Header represents a header parameter.

type Info

type Info struct {
	// Title of the API (required).
	Title string `json:"title"`

	// Version of the OpenAPI document (required).
	Version string `json:"version"`

	// Summary is a short summary of the API.
	Summary string `json:"summary,omitempty"`

	// Description is a description of the API (supports CommonMark).
	Description string `json:"description,omitempty"`

	// Contact information for the exposed API.
	Contact *Contact `json:"contact,omitempty"`

	// License information for the exposed API.
	License *License `json:"license,omitempty"`

	// TermsOfService is a URL to the Terms of Service for the API.
	TermsOfService string `json:"termsOfService,omitempty"`
}

Info provides metadata about the API.

type License

type License struct {
	Name       string `json:"name"`
	Identifier string `json:"identifier,omitempty"`
	URL        string `json:"url,omitempty"`
}

License information for the exposed API.

type MediaType

type MediaType struct {
	// Schema defining the content of the request, response, or parameter.
	Schema *Schema `json:"schema,omitempty"`

	// Example of the media type.
	Example any `json:"example,omitempty"`

	// Examples of the media type.
	Examples map[string]Example `json:"examples,omitempty"`
}

MediaType provides schema and examples for the media type.

type Middleware

type Middleware func(HandlerFunc) HandlerFunc

Middleware is a function that wraps a HandlerFunc. This is an optional pattern - middleware can also be written as HandlerFunc directly.

Example:

func MyMiddleware() Middleware {
    return func(next HandlerFunc) HandlerFunc {
        return func(c *Context) error {
            // Before handler
            err := next(c)
            // After handler
            return err
        }
    }
}

type OAuthFlow

type OAuthFlow struct {
	AuthorizationURL string            `json:"authorizationUrl,omitempty"`
	TokenURL         string            `json:"tokenUrl,omitempty"`
	RefreshURL       string            `json:"refreshUrl,omitempty"`
	Scopes           map[string]string `json:"scopes"`
}

OAuthFlow represents an OAuth flow.

type OAuthFlows

type OAuthFlows struct {
	Implicit          *OAuthFlow `json:"implicit,omitempty"`
	Password          *OAuthFlow `json:"password,omitempty"`
	ClientCredentials *OAuthFlow `json:"clientCredentials,omitempty"`
	AuthorizationCode *OAuthFlow `json:"authorizationCode,omitempty"`
}

OAuthFlows represents OAuth flows.

type OpenAPI

type OpenAPI struct {
	// OpenAPI version string (e.g., "3.1.0").
	OpenAPI string `json:"openapi"`

	// Info provides metadata about the API.
	Info Info `json:"info"`

	// Servers is an array of Server objects providing connectivity information.
	Servers []Server `json:"servers,omitempty"`

	// Paths holds the available paths and operations for the API.
	Paths map[string]PathItem `json:"paths"`

	// Components holds reusable objects for different aspects of the OAS.
	Components *Components `json:"components,omitempty"`

	// Security is a declaration of which security mechanisms can be used across the API.
	Security []SecurityRequirement `json:"security,omitempty"`

	// Tags is a list of tags used by the document with additional metadata.
	Tags []Tag `json:"tags,omitempty"`
}

OpenAPI represents an OpenAPI 3.1 document.

This is the root object of the OpenAPI Description. It provides metadata about the API and describes the available endpoints.

Spec: https://spec.openapis.org/oas/v3.1.1.html

func (*OpenAPI) WriteJSON

func (doc *OpenAPI) WriteJSON(w http.ResponseWriter) error

WriteJSON writes the OpenAPI document as JSON.

func (*OpenAPI) WriteYAML

func (doc *OpenAPI) WriteYAML(w http.ResponseWriter) error

WriteYAML writes the OpenAPI document as YAML. Note: Requires YAML library (not implemented yet as it's not stdlib).

type Operation

type Operation struct {
	// Tags for API documentation control.
	Tags []string `json:"tags,omitempty"`

	// Summary is a short summary of what the operation does.
	Summary string `json:"summary,omitempty"`

	// Description is a verbose explanation of the operation behavior.
	Description string `json:"description,omitempty"`

	// OperationID is a unique string used to identify the operation.
	OperationID string `json:"operationId,omitempty"`

	// Parameters that are applicable for this operation.
	Parameters []Parameter `json:"parameters,omitempty"`

	// RequestBody applicable for this operation.
	RequestBody *RequestBody `json:"requestBody,omitempty"`

	// Responses is the list of possible responses.
	Responses map[string]Response `json:"responses"`

	// Deprecated declares this operation to be deprecated.
	Deprecated bool `json:"deprecated,omitempty"`

	// Security is a declaration of which security mechanisms can be used.
	Security []SecurityRequirement `json:"security,omitempty"`
}

Operation describes a single API operation on a path.

type Param

type Param struct {
	Key   string // Parameter name (e.g., "id" from /:id)
	Value string // Parameter value extracted from path
}

Param represents a URL parameter extracted from the path.

Example:

For route "/users/:id" and path "/users/123":
Param{Key: "id", Value: "123"}

type Parameter

type Parameter struct {
	// Name of the parameter (required).
	Name string `json:"name"`

	// In is the location of the parameter (required).
	// Possible values: "query", "header", "path", "cookie".
	In string `json:"in"`

	// Description of the parameter.
	Description string `json:"description,omitempty"`

	// Required determines whether this parameter is mandatory.
	// Must be true if the parameter location is "path".
	Required bool `json:"required,omitempty"`

	// Deprecated specifies that a parameter is deprecated.
	Deprecated bool `json:"deprecated,omitempty"`

	// Schema defining the type used for the parameter.
	Schema *Schema `json:"schema,omitempty"`
}

Parameter describes a single operation parameter.

type PathItem

type PathItem struct {
	Summary     string      `json:"summary,omitempty"`
	Description string      `json:"description,omitempty"`
	Get         *Operation  `json:"get,omitempty"`
	Post        *Operation  `json:"post,omitempty"`
	Put         *Operation  `json:"put,omitempty"`
	Delete      *Operation  `json:"delete,omitempty"`
	Patch       *Operation  `json:"patch,omitempty"`
	Head        *Operation  `json:"head,omitempty"`
	Options     *Operation  `json:"options,omitempty"`
	Parameters  []Parameter `json:"parameters,omitempty"`
}

PathItem describes operations available on a single path.

type Problem

type Problem struct {
	// Type is a URI reference that identifies the problem type.
	// It should provide human-readable documentation for the problem type.
	// When dereferenced, it SHOULD provide human-readable documentation.
	// Defaults to "about:blank" if not specified.
	Type string `json:"type"`

	// Title is a short, human-readable summary of the problem type.
	// It SHOULD NOT change from occurrence to occurrence of the problem,
	// except for purposes of localization.
	Title string `json:"title"`

	// Status is the HTTP status code generated by the origin server.
	Status int `json:"status"`

	// Detail is a human-readable explanation specific to this occurrence.
	// It SHOULD focus on helping the client correct the problem.
	Detail string `json:"detail,omitempty"`

	// Instance is a URI reference that identifies the specific occurrence.
	// It may or may not yield further information if dereferenced.
	Instance string `json:"instance,omitempty"`

	// Extensions contains additional problem-specific fields.
	// These will be flattened into the JSON output alongside standard fields.
	//
	// Example:
	//   Extensions: map[string]any{
	//       "balance": 30,
	//       "cost": 50,
	//   }
	Extensions map[string]any `json:"-"`
}

Problem represents an RFC 9457 Problem Details object.

RFC 9457 defines a "problem detail" as a way to carry machine-readable details of errors in HTTP response content to avoid the need to define new error response formats for HTTP APIs.

Standard fields:

  • type: URI reference identifying the problem type
  • title: Short, human-readable summary
  • status: HTTP status code
  • detail: Human-readable explanation specific to this occurrence
  • instance: URI reference to the specific occurrence

Extensions can be added via the Extensions map, which will be flattened into the JSON output.

Example:

problem := fursy.Problem{
    Type:   "https://example.com/probs/out-of-credit",
    Title:  "You do not have enough credit",
    Status: 403,
    Detail: "Your current balance is 30, but that costs 50",
    Instance: "/account/12345/msgs/abc",
}

Media Type: application/problem+json

Spec: https://www.rfc-editor.org/rfc/rfc9457.html

func BadRequest

func BadRequest(detail string) Problem

BadRequest creates a 400 Bad Request problem.

func Conflict

func Conflict(detail string) Problem

Conflict creates a 409 Conflict problem.

func Forbidden

func Forbidden(detail string) Problem

Forbidden creates a 403 Forbidden problem.

func InternalServerError

func InternalServerError(detail string) Problem

InternalServerError creates a 500 Internal Server Error problem.

func MethodNotAllowed

func MethodNotAllowed(detail string) Problem

MethodNotAllowed creates a 405 Method Not Allowed problem.

func NewProblem

func NewProblem(status int, title, detail string) Problem

NewProblem creates a new Problem with the given status, title, and detail. The type defaults to "about:blank" as per RFC 9457.

func NotAcceptable

func NotAcceptable(detail string) Problem

NotAcceptable creates a 406 Not Acceptable Problem.

RFC 9110 Section 15.5.7: The 406 Not Acceptable status code indicates that the target resource does not have a current representation that would be acceptable to the user agent, according to the proactive negotiation header fields received in the request, and the server is unwilling to supply a default representation.

func NotFound

func NotFound(detail string) Problem

NotFound creates a 404 Not Found problem.

func ServiceUnavailable

func ServiceUnavailable(detail string) Problem

ServiceUnavailable creates a 503 Service Unavailable problem.

func TooManyRequests

func TooManyRequests(detail string) Problem

TooManyRequests creates a 429 Too Many Requests problem.

func Unauthorized

func Unauthorized(detail string) Problem

Unauthorized creates a 401 Unauthorized problem.

func UnprocessableEntity

func UnprocessableEntity(detail string) Problem

UnprocessableEntity creates a 422 Unprocessable Entity problem. This is commonly used for validation errors.

func ValidationProblem

func ValidationProblem(errs ValidationErrors) Problem

ValidationProblem creates a 422 Unprocessable Entity problem from ValidationErrors.

The validation errors are included as an extension field "errors" containing a map of field names to error messages.

Example output:

{
  "type": "about:blank",
  "title": "Validation Failed",
  "status": 422,
  "detail": "One or more fields failed validation",
  "errors": {
    "email": "must be a valid email address",
    "age": "must be at least 18"
  }
}

func (Problem) Error

func (p Problem) Error() string

Error implements the error interface. Returns the detail if available, otherwise the title.

func (Problem) MarshalJSON

func (p Problem) MarshalJSON() ([]byte, error)

MarshalJSON implements custom JSON marshaling to flatten extensions.

func (Problem) WithExtension

func (p Problem) WithExtension(key string, value any) Problem

WithExtension adds an extension field to the problem.

func (Problem) WithExtensions

func (p Problem) WithExtensions(extensions map[string]any) Problem

WithExtensions sets multiple extension fields at once.

func (Problem) WithInstance

func (p Problem) WithInstance(instance string) Problem

WithInstance sets the instance URI for the problem.

func (Problem) WithType

func (p Problem) WithType(typeURI string) Problem

WithType sets the type URI for the problem.

type RequestBody

type RequestBody struct {
	// Description of the request body.
	Description string `json:"description,omitempty"`

	// Content is a map of media types to media type objects.
	Content map[string]MediaType `json:"content"`

	// Required determines if the request body is required.
	Required bool `json:"required,omitempty"`
}

RequestBody describes a single request body.

type Response

type Response struct {
	// Description of the response (required).
	Description string `json:"description"`

	// Content is a map of media types to media type objects.
	Content map[string]MediaType `json:"content,omitempty"`

	// Headers is a map of response headers.
	Headers map[string]Header `json:"headers,omitempty"`
}

Response describes a single response from an API operation.

type RouteGroup

type RouteGroup struct {
	// contains filtered or unexported fields
}

RouteGroup represents a group of routes that share the same path prefix and middleware. Groups allow organizing routes hierarchically and applying middleware to specific route sets.

Example:

api := router.Group("/api")
api.Use(AuthMiddleware())

v1 := api.Group("/v1")
v1.GET("/users", listUsers)      // GET /api/v1/users
v1.POST("/users", createUser)    // POST /api/v1/users

v2 := api.Group("/v2")
v2.GET("/users", listUsersV2)    // GET /api/v2/users

func (*RouteGroup) DELETE

func (g *RouteGroup) DELETE(path string, handler HandlerFunc)

DELETE registers a DELETE route on the group.

Example:

api := router.Group("/api")
api.DELETE("/users/:id", func(c *Box) error {
    return c.NoContent(204)
})

func (*RouteGroup) GET

func (g *RouteGroup) GET(path string, handler HandlerFunc)

GET registers a GET route on the group.

Example:

api := router.Group("/api")
api.GET("/users", func(c *Box) error {
    return c.JSON(200, users)
})

func (*RouteGroup) Group

func (g *RouteGroup) Group(prefix string, middleware ...HandlerFunc) *RouteGroup

Group creates a new nested route group with the given prefix and optional middleware. The new group's prefix is the combination of the parent prefix and the new prefix. If no middleware is provided, the new group inherits the parent's middleware.

Example:

api := router.Group("/api")
api.Use(LoggerMiddleware())

v1 := api.Group("/v1")              // Inherits logger
v1.Use(AuthMiddleware())            // Adds auth
v1.GET("/users", handler)           // GET /api/v1/users (logger + auth)

v2 := api.Group("/v2", RateLimitMiddleware())  // Custom middleware
v2.GET("/users", handler)           // GET /api/v2/users (ratelimit only)

func (*RouteGroup) HEAD

func (g *RouteGroup) HEAD(path string, handler HandlerFunc)

HEAD registers a HEAD route on the group.

Example:

api := router.Group("/api")
api.HEAD("/users/:id", func(c *Box) error {
    return c.NoContent(200)
})

func (*RouteGroup) Handle

func (g *RouteGroup) Handle(method, path string, handler HandlerFunc)

Handle registers a route with the given HTTP method, path, and handler. This is the core method used by all HTTP method shortcuts (GET, POST, etc.).

The final route path is: group.prefix + path The final middleware chain is: router.middleware + group.middleware + handler

Example:

api := router.Group("/api")
api.Handle("GET", "/users", handler)  // Registers GET /api/users

func (*RouteGroup) OPTIONS

func (g *RouteGroup) OPTIONS(path string, handler HandlerFunc)

OPTIONS registers an OPTIONS route on the group.

Example:

api := router.Group("/api")
api.OPTIONS("/users", func(c *Box) error {
    c.SetHeader("Allow", "GET, POST")
    return c.NoContent(200)
})

func (*RouteGroup) PATCH

func (g *RouteGroup) PATCH(path string, handler HandlerFunc)

PATCH registers a PATCH route on the group.

Example:

api := router.Group("/api")
api.PATCH("/users/:id", func(c *Box) error {
    return c.JSON(200, patchedUser)
})

func (*RouteGroup) POST

func (g *RouteGroup) POST(path string, handler HandlerFunc)

POST registers a POST route on the group.

Example:

api := router.Group("/api")
api.POST("/users", func(c *Box) error {
    return c.JSON(201, newUser)
})

func (*RouteGroup) PUT

func (g *RouteGroup) PUT(path string, handler HandlerFunc)

PUT registers a PUT route on the group.

Example:

api := router.Group("/api")
api.PUT("/users/:id", func(c *Box) error {
    return c.JSON(200, updatedUser)
})

func (*RouteGroup) Use

func (g *RouteGroup) Use(middleware ...HandlerFunc) *RouteGroup

Use registers middleware to the route group. Group middleware is executed after router middleware but before route handlers.

Middleware order: Router.Use() → Group.Use() → Handler

Example:

api := router.Group("/api")
api.Use(LoggerMiddleware())
api.Use(AuthMiddleware())

Can be chained:

api.Use(Logger()).Use(Auth())

type RouteInfo

type RouteInfo struct {
	// Method is the HTTP method (GET, POST, etc.).
	Method string

	// Path is the route path (e.g., "/users/:id").
	Path string

	// Summary is a short description of the operation.
	Summary string

	// Description is a detailed description of the operation.
	Description string

	// Tags are categories for documentation grouping.
	Tags []string

	// OperationID is a unique identifier for the operation.
	OperationID string

	// Deprecated: indicates if this route is deprecated.
	Deprecated bool

	// RequestType is the Go type for the request body (if any).
	RequestType reflect.Type

	// ResponseType is the Go type for the response body (if any).
	ResponseType reflect.Type

	// Parameters stores metadata about path/query/header parameters.
	Parameters []RouteParameter

	// Responses stores metadata about possible responses.
	Responses map[int]RouteResponse
}

RouteInfo stores metadata about a registered route. This information is used for OpenAPI generation, documentation, and introspection.

type RouteOptions

type RouteOptions struct {
	// Summary is a short description of the operation.
	Summary string

	// Description is a detailed description of the operation.
	Description string

	// Tags are categories for documentation grouping.
	Tags []string

	// OperationID is a unique identifier for the operation.
	OperationID string

	// Deprecated: indicates if this route is deprecated.
	Deprecated bool

	// Parameters stores metadata about path/query/header parameters.
	Parameters []RouteParameter

	// Responses stores metadata about possible responses.
	Responses map[int]RouteResponse
}

RouteOptions allows configuring route metadata when registering a route.

type RouteParameter

type RouteParameter struct {
	// Name of the parameter.
	Name string

	// In specifies the location: "path", "query", "header", "cookie".
	In string

	// Description of the parameter.
	Description string

	// Required indicates if the parameter is required.
	Required bool

	// Type is the Go type of the parameter.
	Type reflect.Type
}

RouteParameter stores metadata about a route parameter.

type RouteResponse

type RouteResponse struct {
	// Description of the response.
	Description string

	// Type is the Go type of the response body.
	Type reflect.Type

	// ContentType is the media type (e.g., "application/json").
	ContentType string
}

RouteResponse stores metadata about a response.

type Router

type Router struct {
	// contains filtered or unexported fields
}

Router is the main HTTP router for FURSY. It provides fast URL routing with support for static paths, parameters (:id), and wildcards (*path).

Router implements http.Handler and can be used directly with http.ListenAndServe.

func New

func New() *Router

New creates a new Router instance with default configuration.

The router is created with:

  • Context pooling enabled for zero allocations
  • Method Not Allowed handling enabled
  • OPTIONS handling enabled
  • Empty routing tables (trees are created on first route registration)

func (*Router) DELETE

func (r *Router) DELETE(path string, handler HandlerFunc)

DELETE registers a handler for DELETE requests to the specified path.

Example:

router.DELETE("/users/:id", func(c *fursy.Box) error {
	id := c.Param("id")
	return c.NoContent(204)
})

func (*Router) GET

func (r *Router) GET(path string, handler HandlerFunc)

GET registers a handler for GET requests to the specified path.

Example:

router.GET("/users", func(c *fursy.Box) error {
	return c.JSON(200, users)
})

func (*Router) GenerateOpenAPI

func (r *Router) GenerateOpenAPI(info Info) (*OpenAPI, error)

GenerateOpenAPI generates an OpenAPI 3.1 document from the router.

This method introspects all registered routes and generates a complete OpenAPI 3.1 specification including paths, schemas, and components.

If info is not provided via WithInfo(), the info parameter is used.

Example:

doc, err := router.GenerateOpenAPI(Info{
    Title:   "My API",
    Version: "1.0.0",
})

func (*Router) Group

func (r *Router) Group(prefix string, middleware ...HandlerFunc) *RouteGroup

Group creates a new route group with the given path prefix and optional middleware. Groups allow organizing routes hierarchically and applying middleware to specific route sets.

The group inherits router middleware but can have its own middleware stack. Middleware order: Router.Use() → Group.Use() → Handler

Example:

router := fursy.New()
router.Use(LoggerMiddleware())  // Global

api := router.Group("/api")
api.Use(AuthMiddleware())       // API-specific

v1 := api.Group("/v1")
v1.GET("/users", handler)       // GET /api/v1/users (logger + auth)

admin := api.Group("/admin", AdminMiddleware())  // Custom middleware
admin.GET("/settings", handler)  // GET /api/admin/settings (admin only)

func (*Router) HEAD

func (r *Router) HEAD(path string, handler HandlerFunc)

HEAD registers a handler for HEAD requests to the specified path.

Example:

router.HEAD("/users/:id", func(c *fursy.Box) error {
	return c.NoContent(200)
})

func (*Router) Handle

func (r *Router) Handle(method, path string, handler HandlerFunc)

Handle registers a handler for the given HTTP method and path.

The method must be a valid HTTP method (GET, POST, etc.). The path must start with a '/' and can contain:

  • Static segments: /users
  • Named parameters: /users/:id
  • Catch-all parameters: /files/*path

Panics if method or path is empty, or if handler is nil.

Example:

router.Handle("GET", "/users/:id", func(c *fursy.Box) error {
	id := c.Param("id")
	return c.String(200, "User ID: "+id)
})

func (*Router) HandleWithOptions

func (r *Router) HandleWithOptions(method, path string, handler HandlerFunc, opts *RouteOptions)

HandleWithOptions registers a handler with route metadata for OpenAPI generation.

This method extends Handle() with support for route documentation.

Example:

router.HandleWithOptions("GET", "/users/:id", handler, &RouteOptions{
    Summary:     "Get user by ID",
    Description: "Returns a single user",
    Tags:        []string{"users"},
})

func (*Router) ListenAndServeWithShutdown

func (r *Router) ListenAndServeWithShutdown(addr string, timeout ...time.Duration) error

ListenAndServeWithShutdown starts the HTTP server with automatic graceful shutdown.

This is a convenience method that:

  1. Creates an http.Server with the given address
  2. Listens for SIGTERM and SIGINT signals (Kubernetes/Docker compatible)
  3. Starts the server in a goroutine
  4. Blocks until shutdown signal is received
  5. Calls Shutdown() with the specified timeout (default: 30s)

The timeout must be less than Kubernetes terminationGracePeriodSeconds (default 30s) to allow time for preStop hooks and connection draining.

Returns:

  • nil if shutdown completed successfully
  • http.ErrServerClosed is treated as successful shutdown (not returned)
  • Error from server startup (e.g., address in use)
  • Error from Shutdown if timeout exceeded

Example (simple):

router := fursy.New()
router.GET("/health", healthHandler)

router.OnShutdown(func() {
    log.Println("Closing database...")
    db.Close()
})

// Blocks until SIGTERM/SIGINT, then graceful shutdown with 30s timeout
if err := router.ListenAndServeWithShutdown(":8080"); err != nil {
    log.Fatal(err)
}

Example (custom timeout):

// 10 second shutdown timeout
if err := router.ListenAndServeWithShutdown(":8080", 10*time.Second); err != nil {
    log.Fatal(err)
}

Example (Kubernetes-ready):

router := fursy.New()

// Health check for readiness probe
router.GET("/health", func(c *fursy.Context) error {
    return c.String(200, "OK")
})

// Cleanup on shutdown
router.OnShutdown(func() {
    log.Println("Shutdown initiated...")
    db.Close()
    cache.Close()
    log.Println("Cleanup complete")
})

// Kubernetes sends SIGTERM before killing pod
// terminationGracePeriodSeconds: 30s (default)
// Our shutdown timeout: 25s (leaves 5s buffer)
if err := router.ListenAndServeWithShutdown(":8080", 25*time.Second); err != nil {
    log.Fatal(err)
}

func (*Router) OPTIONS

func (r *Router) OPTIONS(path string, handler HandlerFunc)

OPTIONS registers a handler for OPTIONS requests to the specified path.

Example:

router.OPTIONS("/users", func(c *fursy.Box) error {
	c.SetHeader("Allow", "GET, POST, PUT, DELETE")
	return c.NoContent(200)
})

func (*Router) OnShutdown

func (r *Router) OnShutdown(f func())

OnShutdown registers a function to be called during graceful shutdown.

Callbacks are executed in reverse order (last registered, first called) before the HTTP server stops accepting new connections.

Use this for cleanup tasks like:

  • Closing database connections
  • Flushing logs or metrics
  • Saving in-memory data
  • Releasing external resources

OnShutdown is safe for concurrent use.

Example:

router := fursy.New()

// Register cleanup callbacks
router.OnShutdown(func() {
    log.Println("Closing database connections...")
    db.Close()
})

router.OnShutdown(func() {
    log.Println("Flushing metrics...")
    metrics.Flush()
})

// Start server with graceful shutdown
if err := router.ListenAndServeWithShutdown(":8080"); err != nil {
    log.Fatal(err)
}

func (*Router) PATCH

func (r *Router) PATCH(path string, handler HandlerFunc)

PATCH registers a handler for PATCH requests to the specified path.

Example:

router.PATCH("/users/:id", func(c *fursy.Box) error {
	id := c.Param("id")
	return c.JSON(200, updatedUser)
})

func (*Router) POST

func (r *Router) POST(path string, handler HandlerFunc)

POST registers a handler for POST requests to the specified path.

Example:

router.POST("/users", func(c *fursy.Box) error {
	return c.JSON(201, newUser)
})

func (*Router) PUT

func (r *Router) PUT(path string, handler HandlerFunc)

PUT registers a handler for PUT requests to the specified path.

Example:

router.PUT("/users/:id", func(c *fursy.Box) error {
	id := c.Param("id")
	return c.NoContent(204)
})

func (*Router) ServeHTTP

func (r *Router) ServeHTTP(w http.ResponseWriter, req *http.Request)

ServeHTTP implements http.Handler interface, making Router compatible with the standard library's http.Server.

It performs the following steps:

  1. Gets a Context from the pool (zero allocation)
  2. Looks up the appropriate routing tree for the HTTP method
  3. Searches for a matching route in the radix tree
  4. Extracts URL parameters if the route contains wildcards
  5. Initializes the Context with request/response/params
  6. Builds middleware chain (global middleware + route handler)
  7. Executes the middleware chain via c.Next()
  8. Resets and returns Context to the pool

Returns 404 Not Found if no route matches the path. Returns 405 Method Not Allowed if the path exists but for a different method (when handleMethodNotAllowed is enabled).

func (*Router) ServeOpenAPI

func (r *Router) ServeOpenAPI(path string)

ServeOpenAPI registers a route that serves the OpenAPI 3.1 specification as JSON.

This is a convenience method that automatically generates and serves the OpenAPI document at the specified path. The document is generated on each request, so it always reflects the current state of registered routes.

For production use, consider caching the generated document or serving a pre-generated specification file.

Example:

router := fursy.New()
router.WithInfo(fursy.Info{
    Title:   "My API",
    Version: "1.0.0",
})

// Register your routes
router.GET("/users", handler)
router.POST("/users", handler)

// Serve OpenAPI specification
router.ServeOpenAPI("/openapi.json")

// Now GET /openapi.json returns the OpenAPI 3.1 document

func (*Router) SetServer

func (r *Router) SetServer(srv *http.Server)

SetServer sets the http.Server for graceful shutdown.

This is typically called by ListenAndServeWithShutdown, but can be used manually if you create the server yourself.

Example:

router := fursy.New()
srv := &http.Server{
    Addr:         ":8080",
    Handler:      router,
    ReadTimeout:  10 * time.Second,
    WriteTimeout: 10 * time.Second,
}

router.SetServer(srv)
router.OnShutdown(func() {
    log.Println("Server shutting down...")
})

// Now router.Shutdown() will shutdown srv

func (*Router) SetValidator

func (r *Router) SetValidator(v Validator) *Router

SetValidator sets the validator for automatic request validation.

When a validator is set, Box.Bind() will automatically validate request bodies after binding. If validation fails, Bind() returns a ValidationErrors error.

Validator is optional. If not set, binding works without validation.

Example:

// Using validator/v10 (requires plugin)
import "github.com/coregx/fursy/plugins/validator"

router := fursy.New()
router.SetValidator(validator.New())

// Now all POST/PUT/PATCH requests will be validated
type CreateUserRequest struct {
    Email string `json:"email" validate:"required,email"`
    Age   int    `json:"age" validate:"gte=18,lte=120"`
}

POST[CreateUserRequest, UserResponse](router, "/users", func(c *Box[CreateUserRequest, UserResponse]) error {
    // c.ReqBody is already validated here!
    // ...
})

func (*Router) Shutdown

func (r *Router) Shutdown(ctx context.Context) error

Shutdown gracefully shuts down the HTTP server and executes registered callbacks.

Shutdown works in two phases:

  1. Calls all registered OnShutdown callbacks in reverse order
  2. Calls http.Server.Shutdown() to gracefully stop the server

The server shutdown process:

  • Immediately closes all listeners (stops accepting new connections)
  • Waits for active requests to complete (respects context timeout)
  • Returns context error if timeout is exceeded

Shutdown does NOT forcefully close active connections after timeout. If you need to force close, cancel the context.

Safe to call multiple times (subsequent calls are no-ops).

Example:

router := fursy.New()
router.OnShutdown(func() {
    log.Println("Cleanup...")
})

// Start server in goroutine
srv := &http.Server{Addr: ":8080", Handler: router}
router.SetServer(srv)

go func() {
    if err := srv.ListenAndServe(); err != nil && !errors.Is(err, http.ErrServerClosed) {
        log.Fatal(err)
    }
}()

// Handle shutdown signal
sigChan := make(chan os.Signal, 1)
signal.Notify(sigChan, syscall.SIGTERM, syscall.SIGINT)
<-sigChan

// Graceful shutdown with 30s timeout
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
defer cancel()

if err := router.Shutdown(ctx); err != nil {
    log.Printf("Shutdown error: %v", err)
}

func (*Router) Use

func (r *Router) Use(middleware ...HandlerFunc) *Router

Use registers global middleware that executes for all routes. Middleware is executed in the order it is registered.

Middleware can:

  • Modify the request/response
  • Call c.Next() to continue the chain
  • Call c.Abort() to stop the chain
  • Return an error to propagate to error handler

Example:

router.Use(Logger())
router.Use(Recovery())
router.Use(CORS())

func Logger() HandlerFunc {
    return func(c *Context) error {
        start := time.Now()
        err := c.Next()
        log.Printf("%s - %v", c.Request.URL.Path, time.Since(start))
        return err
    }
}

func (*Router) WithInfo

func (r *Router) WithInfo(info Info) *Router

WithInfo sets the API metadata for OpenAPI generation.

This configures the info section of the generated OpenAPI document.

Example:

router.WithInfo(Info{
    Title:       "My API",
    Version:     "1.0.0",
    Description: "A sample API built with FURSY",
})

func (*Router) WithServer

func (r *Router) WithServer(server Server) *Router

WithServer adds a server to the OpenAPI document.

Servers define the base URLs where the API is deployed.

Example:

router.WithServer(Server{
    URL:         "https://api.example.com",
    Description: "Production server",
})

type Schema

type Schema struct {
	// Type specifies the data type.
	Type string `json:"type,omitempty"`

	// Format provides additional type information.
	Format string `json:"format,omitempty"`

	// Title of the schema.
	Title string `json:"title,omitempty"`

	// Description of the schema.
	Description string `json:"description,omitempty"`

	// Properties for object types (property name -> schema).
	Properties map[string]*Schema `json:"properties,omitempty"`

	// Required lists required properties for object types.
	Required []string `json:"required,omitempty"`

	// Items schema for array types.
	Items *Schema `json:"items,omitempty"`

	// Enum restricts values to a specific set.
	Enum []any `json:"enum,omitempty"`

	// Default value.
	Default any `json:"default,omitempty"`

	// Example value.
	Example any `json:"example,omitempty"`

	// Nullable indicates if the value can be null.
	Nullable bool `json:"nullable,omitempty"`

	// ReadOnly indicates the property is read-only.
	ReadOnly bool `json:"readOnly,omitempty"`

	// WriteOnly indicates the property is write-only.
	WriteOnly bool `json:"writeOnly,omitempty"`

	// Ref is a reference to another schema.
	Ref string `json:"$ref,omitempty"`

	// AdditionalProperties for object types.
	AdditionalProperties any `json:"additionalProperties,omitempty"`

	// OneOf specifies that the value must match exactly one schema.
	OneOf []*Schema `json:"oneOf,omitempty"`

	// AnyOf specifies that the value must match at least one schema.
	AnyOf []*Schema `json:"anyOf,omitempty"`

	// AllOf specifies that the value must match all schemas.
	AllOf []*Schema `json:"allOf,omitempty"`
}

Schema represents a data type schema. This is compatible with JSON Schema Draft 2020-12.

type SecurityRequirement

type SecurityRequirement map[string][]string

SecurityRequirement represents a security requirement.

type SecurityScheme

type SecurityScheme struct {
	Type             string      `json:"type"`
	Description      string      `json:"description,omitempty"`
	Name             string      `json:"name,omitempty"`
	In               string      `json:"in,omitempty"`
	Scheme           string      `json:"scheme,omitempty"`
	BearerFormat     string      `json:"bearerFormat,omitempty"`
	Flows            *OAuthFlows `json:"flows,omitempty"`
	OpenIDConnectURL string      `json:"openIdConnectUrl,omitempty"`
}

SecurityScheme defines a security scheme.

type Server

type Server struct {
	// URL to the target host.
	URL string `json:"url"`

	// Description is an optional string describing the host.
	Description string `json:"description,omitempty"`

	// Variables for server URL template substitution.
	Variables map[string]ServerVariable `json:"variables,omitempty"`
}

Server represents a server.

type ServerVariable

type ServerVariable struct {
	Enum        []string `json:"enum,omitempty"`
	Default     string   `json:"default"`
	Description string   `json:"description,omitempty"`
}

ServerVariable represents a server variable for template substitution.

type Tag

type Tag struct {
	Name        string `json:"name"`
	Description string `json:"description,omitempty"`
}

Tag represents a tag with metadata.

type ValidationError

type ValidationError struct {
	// Field is the name of the field that failed validation.
	// For nested structs, uses dot notation (e.g., "Address.City").
	Field string `json:"field"`

	// Tag is the validation rule that failed (e.g., "required", "email", "min").
	Tag string `json:"tag"`

	// Value is the actual value that failed validation.
	// Omitted from JSON if nil to avoid exposing sensitive data.
	Value any `json:"value,omitempty"`

	// Message is a human-readable error message.
	Message string `json:"message"`
}

ValidationError represents a single field validation error.

It provides structured information about what field failed validation, which rule was violated, and a human-readable message.

func (*ValidationError) Error

func (ve *ValidationError) Error() string

Error implements the error interface.

type ValidationErrors

type ValidationErrors []ValidationError

ValidationErrors is a collection of validation errors.

It implements the error interface and provides methods for error formatting.

func (*ValidationErrors) Add

func (ve *ValidationErrors) Add(field, tag, message string)

Add adds a validation error to the collection.

func (*ValidationErrors) AddError

func (ve *ValidationErrors) AddError(err ValidationError)

AddError adds a validation error struct to the collection.

func (ValidationErrors) Error

func (ve ValidationErrors) Error() string

Error implements the error interface. Returns a concatenated string of all validation errors.

func (ValidationErrors) Fields

func (ve ValidationErrors) Fields() map[string]string

Fields returns a map of field names to their error messages. Useful for API responses with per-field error details.

Example:

{
  "email": "must be a valid email address",
  "age": "must be at least 18"
}

func (ValidationErrors) IsEmpty

func (ve ValidationErrors) IsEmpty() bool

IsEmpty returns true if there are no validation errors.

type Validator

type Validator interface {
	// Validate validates the given struct and returns validation errors.
	// Returns nil if validation passes.
	// Returns ValidationErrors for field-level errors.
	Validate(any) error
}

Validator is the interface for request validation.

Implementations can use any validation library (validator/v10, ozzo-validation, custom, etc.) or implement custom validation logic.

Example implementations:

  • plugins/validator: go-playground/validator/v10 integration
  • Custom validator with business rules

type Version

type Version struct {
	Major int
	Minor int
	Patch int
}

Version represents an API version.

FURSY supports semantic versioning (MAJOR.MINOR.PATCH) but commonly uses only major versions for API versioning (v1, v2, etc.).

Example:

v1 := fursy.Version{Major: 1}
v2 := fursy.Version{Major: 2, Minor: 1}

func ExtractVersionFromPath

func ExtractVersionFromPath(path string) (Version, bool)

ExtractVersionFromPath extracts version from URL path.

Supports patterns:

  • /v1/users -> v1
  • /api/v2/posts -> v2
  • /api/v2.1/posts -> v2.1

Returns zero Version and false if no version found.

func ParseVersion

func ParseVersion(s string) (Version, bool)

ParseVersion parses a version string into a Version struct.

Supported formats:

  • "v1" -> Version{Major: 1}
  • "v2.1" -> Version{Major: 2, Minor: 1}
  • "v3.2.1" -> Version{Major: 3, Minor: 2, Patch: 1}
  • "1" -> Version{Major: 1}
  • "2.1.0" -> Version{Major: 2, Minor: 1}

Returns zero Version and false if parsing fails.

func (Version) Equal

func (v Version) Equal(other Version) bool

Equal checks if two versions are equal.

func (Version) GreaterThan

func (v Version) GreaterThan(other Version) bool

GreaterThan checks if this version is greater than another.

func (Version) LessThan

func (v Version) LessThan(other Version) bool

LessThan checks if this version is less than another.

func (Version) String

func (v Version) String() string

String returns the version as a string (e.g., "v1", "v2.1", "v3.2.1").

Directories

Path Synopsis
internal
binding
Package binding provides request body binding functionality.
Package binding provides request body binding functionality.
negotiate
Package negotiate provides RFC 9110 compliant content negotiation.
Package negotiate provides RFC 9110 compliant content negotiation.
radix
Package radix implements a radix tree (compressed trie) for HTTP routing.
Package radix implements a radix tree (compressed trie) for HTTP routing.
Package middleware provides HTTP Basic Authentication middleware.
Package middleware provides HTTP Basic Authentication middleware.
plugins
database module
validator module

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL