Router

Advanced Features

Explore advanced router features including OpenAPI documentation, AsyncAPI support, WebSocket handling, streaming responses, and more.

Advanced Features

Forge's router provides powerful advanced features for modern web applications, including automatic API documentation generation, real-time communication, and streaming capabilities.

OpenAPI Documentation

Basic OpenAPI Setup

Enable automatic OpenAPI 3.1.0 specification generation:

package main

import (
    "github.com/xraph/forge"
)

func main() {
    app := forge.New(
        forge.WithOpenAPI(forge.OpenAPIConfig{
            Title:       "My API",
            Version:     "1.0.0",
            Description: "A comprehensive API built with Forge",
            Servers: []forge.OpenAPIServer{
                {
                    URL:         "https://api.example.com",
                    Description: "Production server",
                },
                {
                    URL:         "http://localhost:8080",
                    Description: "Development server",
                },
            },
            UIPath:   "/docs",     // Swagger UI path
            SpecPath: "/openapi.json", // OpenAPI spec endpoint
        }),
    )
    
    router := app.Router()
    
    // Routes with OpenAPI documentation
    router.POST("/users", createUser,
        forge.WithSummary("Create a new user"),
        forge.WithDescription("Creates a new user with the provided information"),
        forge.WithTags("users"),
        forge.WithOperationID("createUser"),
    )
    
    app.Start(":8080")
}

Advanced OpenAPI Features

// Automatic schema extraction from structs
type User struct {
    ID       string    `json:"id" example:"123e4567-e89b-12d3-a456-426614174000"`
    Name     string    `json:"name" example:"John Doe" validate:"required,min=2,max=100"`
    Email    string    `json:"email" example:"john@example.com" validate:"required,email"`
    Age      int       `json:"age" example:"30" validate:"min=18,max=120"`
    Role     string    `json:"role" example:"user" enums:"user,admin,moderator"`
    Active   bool      `json:"active" example:"true"`
    CreatedAt time.Time `json:"createdAt" example:"2024-01-15T10:30:00Z"`
}

type CreateUserRequest struct {
    Name  string `json:"name" validate:"required,min=2,max=100"`
    Email string `json:"email" validate:"required,email"`
    Age   int    `json:"age" validate:"min=18,max=120"`
    Role  string `json:"role" validate:"required,oneof=user admin moderator"`
}

// Handler with automatic schema detection
func createUser(ctx *forge.Context) error {
    var req CreateUserRequest
    if err := ctx.Bind(&req); err != nil {
        return ctx.JSON(400, map[string]string{"error": "Invalid request"})
    }
    
    user := User{
        ID:        generateID(),
        Name:      req.Name,
        Email:     req.Email,
        Age:       req.Age,
        Role:      req.Role,
        Active:    true,
        CreatedAt: time.Now(),
    }
    
    return ctx.JSON(201, user)
}

// Register with comprehensive documentation
router.POST("/users", createUser,
    forge.WithSummary("Create a new user"),
    forge.WithDescription("Creates a new user with the provided information"),
    forge.WithTags("users", "management"),
    forge.WithOperationID("createUser"),
    forge.WithRequestSchema(&CreateUserRequest{}),
    forge.WithResponseSchema(201, "User created successfully", &User{}),
    forge.WithErrorResponses(),
)
// Add request and response examples
router.POST("/users", createUser,
    forge.WithRequestExample("basic", map[string]interface{}{
        "name":  "John Doe",
        "email": "john@example.com",
        "age":   30,
        "role":  "user",
    }),
    forge.WithRequestExample("admin", map[string]interface{}{
        "name":  "Admin User",
        "email": "admin@example.com",
        "age":   35,
        "role":  "admin",
    }),
    forge.WithResponseExample(201, "success", map[string]interface{}{
        "id":        "123e4567-e89b-12d3-a456-426614174000",
        "name":      "John Doe",
        "email":     "john@example.com",
        "age":       30,
        "role":      "user",
        "active":    true,
        "createdAt": "2024-01-15T10:30:00Z",
    }),
    forge.WithResponseExample(400, "validation_error", map[string]interface{}{
        "error": "Validation failed",
        "details": []map[string]string{
            {"field": "email", "message": "Invalid email format"},
            {"field": "age", "message": "Must be at least 18"},
        },
    }),
)
// Configure security schemes
app := forge.New(
    forge.WithOpenAPI(forge.OpenAPIConfig{
        Title:   "Secure API",
        Version: "1.0.0",
        SecuritySchemes: map[string]forge.SecurityScheme{
            "bearerAuth": {
                Type:         "http",
                Scheme:       "bearer",
                BearerFormat: "JWT",
                Description:  "JWT token authentication",
            },
            "apiKey": {
                Type: "apiKey",
                In:   "header",
                Name: "X-API-Key",
            },
            "oauth2": {
                Type: "oauth2",
                Flows: forge.OAuthFlows{
                    AuthorizationCode: &forge.OAuthFlow{
                        AuthorizationURL: "https://auth.example.com/oauth/authorize",
                        TokenURL:         "https://auth.example.com/oauth/token",
                        Scopes: map[string]string{
                            "read":  "Read access",
                            "write": "Write access",
                            "admin": "Admin access",
                        },
                    },
                },
            },
        },
    }),
)

// Apply security to routes
router.GET("/users", listUsers,
    forge.WithSecurity(map[string][]string{
        "bearerAuth": {},
    }),
)

router.POST("/admin/users", createAdminUser,
    forge.WithSecurity(map[string][]string{
        "bearerAuth": {"admin"},
        "oauth2":     {"admin"},
    }),
)

Polymorphic Types and Discriminators

// Base type
type Animal struct {
    Type string `json:"type" discriminator:"true"`
    Name string `json:"name"`
}

// Derived types
type Dog struct {
    Animal
    Breed string `json:"breed"`
}

type Cat struct {
    Animal
    IndoorOnly bool `json:"indoorOnly"`
}

// Configure discriminator
router.POST("/animals", createAnimal,
    forge.WithDiscriminator(forge.DiscriminatorConfig{
        PropertyName: "type",
        Mapping: map[string]string{
            "dog": "#/components/schemas/Dog",
            "cat": "#/components/schemas/Cat",
        },
    }),
)

Callbacks and Webhooks

// Define callback configuration
router.POST("/orders", createOrder,
    forge.WithCallback(forge.CallbackConfig{
        Name: "orderStatusUpdate",
        URL:  "{$request.body#/webhookUrl}",
        Operations: map[string]forge.CallbackOperation{
            "post": {
                Summary:     "Order status update",
                Description: "Called when order status changes",
                RequestBody: &OrderStatusUpdate{},
                Responses: map[string]forge.ResponseSchemaDef{
                    "200": {Description: "Webhook received"},
                    "400": {Description: "Invalid webhook payload"},
                },
            },
        },
    }),
)

type OrderStatusUpdate struct {
    OrderID string `json:"orderId"`
    Status  string `json:"status"`
    UpdatedAt time.Time `json:"updatedAt"`
}

AsyncAPI Documentation

WebSocket Documentation

Enable AsyncAPI 3.0.0 specification for real-time APIs:

app := forge.New(
    forge.WithAsyncAPI(forge.AsyncAPIConfig{
        Title:       "Real-time API",
        Version:     "1.0.0",
        Description: "WebSocket and SSE endpoints",
        Servers: map[string]forge.AsyncAPIServer{
            "websocket": {
                URL:      "ws://localhost:8080",
                Protocol: "ws",
                Description: "WebSocket server",
            },
            "sse": {
                URL:      "http://localhost:8080",
                Protocol: "sse",
                Description: "Server-Sent Events",
            },
        },
        UIPath:   "/asyncapi",
        SpecPath: "/asyncapi.json",
    }),
)

// WebSocket endpoint with AsyncAPI documentation
router.WebSocket("/chat", handleChat,
    forge.WithAsyncAPISummary("Chat WebSocket"),
    forge.WithAsyncAPIDescription("Real-time chat communication"),
    forge.WithWebSocketMessages(&ChatMessage{}, &ChatResponse{}),
    forge.WithAsyncAPITags("chat", "realtime"),
)

type ChatMessage struct {
    Type    string `json:"type" example:"message"`
    Content string `json:"content" example:"Hello, world!"`
    UserID  string `json:"userId" example:"user123"`
}

type ChatResponse struct {
    Type      string    `json:"type" example:"message"`
    Content   string    `json:"content" example:"Hello, world!"`
    UserID    string    `json:"userId" example:"user123"`
    Timestamp time.Time `json:"timestamp" example:"2024-01-15T10:30:00Z"`
}

Server-Sent Events (SSE)

// SSE endpoint with AsyncAPI documentation
router.EventStream("/events", handleEvents,
    forge.WithAsyncAPISummary("Event Stream"),
    forge.WithAsyncAPIDescription("Server-sent events for real-time updates"),
    forge.WithAsyncAPIChannelName("events"),
    forge.WithAsyncAPITags("events", "streaming"),
)

func handleEvents(ctx *forge.Context) error {
    // Set SSE headers
    ctx.SetHeader("Content-Type", "text/event-stream")
    ctx.SetHeader("Cache-Control", "no-cache")
    ctx.SetHeader("Connection", "keep-alive")
    
    // Create event channel
    events := make(chan Event, 10)
    
    // Start event producer
    go func() {
        ticker := time.NewTicker(5 * time.Second)
        defer ticker.Stop()
        
        for {
            select {
            case <-ticker.C:
                events <- Event{
                    Type: "heartbeat",
                    Data: map[string]interface{}{
                        "timestamp": time.Now(),
                        "status":    "alive",
                    },
                }
            case <-ctx.Done():
                return
            }
        }
    }()
    
    // Stream events
    for event := range events {
        data, _ := json.Marshal(event.Data)
        fmt.Fprintf(ctx.Writer(), "event: %s\ndata: %s\n\n", event.Type, data)
        ctx.Writer().Flush()
    }
    
    return nil
}

type Event struct {
    Type string      `json:"type"`
    Data interface{} `json:"data"`
}

WebSocket Support

Basic WebSocket Handler

func handleWebSocket(ctx *forge.Context) error {
    // Upgrade connection to WebSocket
    conn, err := ctx.UpgradeWebSocket()
    if err != nil {
        return err
    }
    defer conn.Close()
    
    // Handle messages
    for {
        var msg ChatMessage
        if err := conn.ReadJSON(&msg); err != nil {
            if websocket.IsUnexpectedCloseError(err, websocket.CloseGoingAway, websocket.CloseAbnormalClosure) {
                ctx.Logger().Error("WebSocket error", "error", err)
            }
            break
        }
        
        // Process message
        response := ChatResponse{
            Type:      "response",
            Content:   fmt.Sprintf("Echo: %s", msg.Content),
            UserID:    msg.UserID,
            Timestamp: time.Now(),
        }
        
        // Send response
        if err := conn.WriteJSON(response); err != nil {
            ctx.Logger().Error("Write error", "error", err)
            break
        }
    }
    
    return nil
}

// Register WebSocket route
router.WebSocket("/ws/chat", handleWebSocket,
    forge.WithName("chatWebSocket"),
    forge.WithAsyncAPISummary("Chat WebSocket"),
    forge.WithWebSocketMessages(&ChatMessage{}, &ChatResponse{}),
)

Advanced WebSocket Patterns

// WebSocket room management
type ChatRoom struct {
    ID      string
    clients map[*websocket.Conn]string
    mu      sync.RWMutex
}

var rooms = make(map[string]*ChatRoom)
var roomsMu sync.RWMutex

func handleChatRoom(ctx *forge.Context) error {
    roomID := ctx.Param("roomId")
    userID := ctx.Query("userId")
    
    conn, err := ctx.UpgradeWebSocket()
    if err != nil {
        return err
    }
    defer conn.Close()
    
    // Join room
    room := getOrCreateRoom(roomID)
    room.addClient(conn, userID)
    defer room.removeClient(conn)
    
    // Handle messages
    for {
        var msg ChatMessage
        if err := conn.ReadJSON(&msg); err != nil {
            break
        }
        
        // Broadcast to room
        room.broadcast(msg)
    }
    
    return nil
}

func (r *ChatRoom) addClient(conn *websocket.Conn, userID string) {
    r.mu.Lock()
    defer r.mu.Unlock()
    r.clients[conn] = userID
}

func (r *ChatRoom) removeClient(conn *websocket.Conn) {
    r.mu.Lock()
    defer r.mu.Unlock()
    delete(r.clients, conn)
}

func (r *ChatRoom) broadcast(msg ChatMessage) {
    r.mu.RLock()
    defer r.mu.RUnlock()
    
    for conn := range r.clients {
        if err := conn.WriteJSON(msg); err != nil {
            conn.Close()
            delete(r.clients, conn)
        }
    }
}

// Register room-based WebSocket
router.WebSocket("/ws/rooms/:roomId", handleChatRoom,
    forge.WithAsyncAPISummary("Chat Room WebSocket"),
    forge.WithAsyncAPIDescription("Join a chat room for real-time communication"),
)
// WebSocket with authentication
func authenticatedWebSocket(ctx *forge.Context) error {
    // Authenticate before upgrade
    token := ctx.Query("token")
    if token == "" {
        return ctx.JSON(401, map[string]string{
            "error": "Authentication token required",
        })
    }
    
    user, err := validateToken(token)
    if err != nil {
        return ctx.JSON(401, map[string]string{
            "error": "Invalid token",
        })
    }
    
    // Upgrade to WebSocket
    conn, err := ctx.UpgradeWebSocket()
    if err != nil {
        return err
    }
    defer conn.Close()
    
    // Set user context
    ctx.Set("user", user)
    
    // Handle authenticated messages
    for {
        var msg AuthenticatedMessage
        if err := conn.ReadJSON(&msg); err != nil {
            break
        }
        
        // Process with user context
        response := processAuthenticatedMessage(msg, user)
        conn.WriteJSON(response)
    }
    
    return nil
}

type AuthenticatedMessage struct {
    Type    string      `json:"type"`
    Payload interface{} `json:"payload"`
}

Streaming Responses

Chunked Transfer Encoding

func streamLargeData(ctx *forge.Context) error {
    // Set streaming headers
    ctx.SetHeader("Content-Type", "application/json")
    ctx.SetHeader("Transfer-Encoding", "chunked")
    
    // Start JSON array
    ctx.Writer().Write([]byte("["))
    
    // Stream data chunks
    for i := 0; i < 1000; i++ {
        if i > 0 {
            ctx.Writer().Write([]byte(","))
        }
        
        item := map[string]interface{}{
            "id":    i,
            "data":  fmt.Sprintf("Item %d", i),
            "timestamp": time.Now(),
        }
        
        data, _ := json.Marshal(item)
        ctx.Writer().Write(data)
        ctx.Writer().Flush()
        
        // Simulate processing time
        time.Sleep(10 * time.Millisecond)
    }
    
    // End JSON array
    ctx.Writer().Write([]byte("]"))
    
    return nil
}

router.GET("/stream/data", streamLargeData,
    forge.WithSummary("Stream large dataset"),
    forge.WithDescription("Streams a large dataset using chunked transfer encoding"),
)

File Streaming

func streamFile(ctx *forge.Context) error {
    filename := ctx.Param("filename")
    
    // Open file
    file, err := os.Open(filepath.Join("uploads", filename))
    if err != nil {
        return ctx.JSON(404, map[string]string{
            "error": "File not found",
        })
    }
    defer file.Close()
    
    // Get file info
    stat, err := file.Stat()
    if err != nil {
        return ctx.JSON(500, map[string]string{
            "error": "Failed to get file info",
        })
    }
    
    // Set headers
    ctx.SetHeader("Content-Type", "application/octet-stream")
    ctx.SetHeader("Content-Length", fmt.Sprintf("%d", stat.Size()))
    ctx.SetHeader("Content-Disposition", fmt.Sprintf("attachment; filename=%s", filename))
    
    // Stream file
    _, err = io.Copy(ctx.Writer(), file)
    return err
}

router.GET("/files/:filename", streamFile,
    forge.WithSummary("Download file"),
    forge.WithDescription("Streams a file for download"),
    forge.WithTags("files"),
)

WebTransport Support

WebTransport provides low-latency, bidirectional communication over HTTP/3 with QUIC transport.

// Enable WebTransport
app := forge.New(
    forge.WithWebTransport(forge.WebTransportConfig{
        Enabled: true,
        CertFile: "server.crt",
        KeyFile:  "server.key",
    }),
)

// WebTransport handler
func handleWebTransport(ctx *forge.Context) error {
    session, err := ctx.AcceptWebTransport()
    if err != nil {
        return err
    }
    defer session.Close()
    
    // Handle bidirectional streams
    for {
        stream, err := session.AcceptStream()
        if err != nil {
            break
        }
        
        go handleWebTransportStream(stream)
    }
    
    return nil
}

func handleWebTransportStream(stream WebTransportStream) {
    defer stream.Close()
    
    // Read data
    buffer := make([]byte, 1024)
    n, err := stream.Read(buffer)
    if err != nil {
        return
    }
    
    // Process and respond
    response := fmt.Sprintf("Echo: %s", string(buffer[:n]))
    stream.Write([]byte(response))
}

// Register WebTransport endpoint
router.WebTransport("/wt/echo", handleWebTransport,
    forge.WithSummary("WebTransport Echo"),
    forge.WithDescription("Bidirectional communication over WebTransport"),
)

Route Inspection and Debugging

Route Information

// Get all registered routes
routes := router.Routes()
for _, route := range routes {
    fmt.Printf("Route: %s %s -> %s\n", 
        route.Method, 
        route.Pattern, 
        route.Name,
    )
}

// Find route by name
if route, found := router.RouteByName("createUser"); found {
    fmt.Printf("Found route: %s %s\n", route.Method, route.Pattern)
}

// Find routes by tag
userRoutes := router.RoutesByTag("users")
for _, route := range userRoutes {
    fmt.Printf("User route: %s %s\n", route.Method, route.Pattern)
}

// Find routes by metadata
apiRoutes := router.RoutesByMetadata("version", "v1")
for _, route := range apiRoutes {
    fmt.Printf("API v1 route: %s %s\n", route.Method, route.Pattern)
}

Debug Middleware

func debugMiddleware(next forge.HandlerFunc) forge.HandlerFunc {
    return func(ctx *forge.Context) error {
        start := time.Now()
        
        // Log request
        ctx.Logger().Info("Request started",
            "method", ctx.Method(),
            "path", ctx.Path(),
            "ip", ctx.ClientIP(),
            "user_agent", ctx.GetHeader("User-Agent"),
        )
        
        // Execute handler
        err := next(ctx)
        
        // Log response
        duration := time.Since(start)
        ctx.Logger().Info("Request completed",
            "method", ctx.Method(),
            "path", ctx.Path(),
            "status", ctx.Response().Status(),
            "duration", duration,
            "error", err,
        )
        
        return err
    }
}

// Apply debug middleware globally
router.Use(debugMiddleware)

Performance Optimization

Connection Pooling

// Configure connection limits for WebSocket
app := forge.New(
    forge.WithWebSocketConfig(forge.WebSocketConfig{
        MaxConnections:    1000,
        ReadBufferSize:    1024,
        WriteBufferSize:   1024,
        HandshakeTimeout:  10 * time.Second,
        CheckOrigin: func(r *http.Request) bool {
            // Implement origin checking
            return true
        },
    }),
)

Compression

// Enable compression middleware
router.Use(forge.Compression(forge.CompressionConfig{
    Level:     6, // Compression level (1-9)
    MinLength: 1024, // Minimum response size to compress
    Types: []string{
        "application/json",
        "text/html",
        "text/css",
        "application/javascript",
    },
}))

Best Practices

Advanced Features Best Practices

  1. Documentation: Always document WebSocket and SSE endpoints with AsyncAPI
  2. Authentication: Implement proper authentication for real-time endpoints
  3. Rate Limiting: Apply rate limiting to prevent abuse
  4. Error Handling: Handle connection errors gracefully
  5. Resource Cleanup: Always clean up connections and resources
  6. Monitoring: Monitor connection counts and performance metrics

Error Handling for Streaming

func robustWebSocketHandler(ctx *forge.Context) error {
    conn, err := ctx.UpgradeWebSocket()
    if err != nil {
        return err
    }
    
    // Set connection limits
    conn.SetReadLimit(512)
    conn.SetReadDeadline(time.Now().Add(60 * time.Second))
    conn.SetPongHandler(func(string) error {
        conn.SetReadDeadline(time.Now().Add(60 * time.Second))
        return nil
    })
    
    // Ping ticker for keepalive
    ticker := time.NewTicker(54 * time.Second)
    defer ticker.Stop()
    
    // Error handling
    defer func() {
        if r := recover(); r != nil {
            ctx.Logger().Error("WebSocket panic", "error", r)
        }
        conn.Close()
    }()
    
    // Message handling with error recovery
    for {
        select {
        case <-ticker.C:
            if err := conn.WriteMessage(websocket.PingMessage, nil); err != nil {
                return err
            }
        default:
            var msg interface{}
            if err := conn.ReadJSON(&msg); err != nil {
                if websocket.IsUnexpectedCloseError(err, websocket.CloseGoingAway, websocket.CloseAbnormalClosure) {
                    ctx.Logger().Error("WebSocket error", "error", err)
                }
                return err
            }
            
            // Process message safely
            if err := processMessage(msg); err != nil {
                ctx.Logger().Error("Message processing error", "error", err)
                continue
            }
        }
    }
}

Next Steps

How is this guide?

Last updated on