# Rate Limiting Guide Comprehensive guide for configuring and using the rate limiting middleware in Junhong CMP Fiber. ## Table of Contents - [Overview](#overview) - [Configuration](#configuration) - [Storage Options](#storage-options) - [Code Examples](#code-examples) - [Testing](#testing) - [Common Usage Patterns](#common-usage-patterns) - [Monitoring](#monitoring) - [Troubleshooting](#troubleshooting) --- ## Overview The rate limiting middleware protects your API from abuse by limiting the number of requests a client can make within a specified time window. It operates at the IP address level, ensuring each client has independent rate limits. ### Key Features - **IP-based rate limiting**: Each client IP has independent counters - **Configurable limits**: Customize max requests and time windows - **Multiple storage backends**: In-memory or Redis-based storage - **Fail-safe operation**: Continues with in-memory storage if Redis fails - **Hot-reloadable**: Change limits without restarting server - **Unified error responses**: Returns 429 with standardized error format ### How It Works ``` Client Request → Check IP Address → Check Request Count → Allow/Reject ↓ ↓ 192.168.1.1 Counter: 45 / Max: 100 ↓ Allow (increment to 46) ``` ### Rate Limit Algorithm The middleware uses a **sliding window** approach: 1. Extract client IP from request 2. Check counter for IP in storage (key: `rate_limit:{ip}`) 3. If counter < max: increment counter and allow request 4. If counter >= max: reject with 429 status 5. Counter automatically resets after `expiration` duration --- ## Configuration ### Basic Configuration Structure Rate limiting is configured in `configs/config.yaml`: ```yaml middleware: # Enable/disable rate limiting enable_rate_limiter: false # Default: disabled # Rate limiter settings rate_limiter: max: 100 # Maximum requests per window expiration: "1m" # Time window duration storage: "memory" # Storage backend: "memory" or "redis" ``` ### Configuration Parameters #### `enable_rate_limiter` (boolean) Controls whether rate limiting is active. - **Default**: `false` - **Values**: `true` (enabled), `false` (disabled) - **Hot-reloadable**: Yes **Example**: ```yaml middleware: enable_rate_limiter: true # Enable rate limiting ``` #### `max` (integer) Maximum number of requests allowed per time window. - **Default**: 100 - **Range**: 1 - unlimited (practical max: ~1,000,000) - **Hot-reloadable**: Yes **Examples**: ```yaml # Strict limit for public APIs rate_limiter: max: 60 # 60 requests per minute # Relaxed limit for internal APIs rate_limiter: max: 5000 # 5000 requests per minute ``` #### `expiration` (duration string) Time window for rate limiting. After this duration, the counter resets. - **Default**: `"1m"` (1 minute) - **Supported formats**: - `"30s"` - 30 seconds - `"1m"` - 1 minute - `"5m"` - 5 minutes - `"1h"` - 1 hour - `"24h"` - 24 hours - **Hot-reloadable**: Yes **Examples**: ```yaml # Short window for burst protection rate_limiter: expiration: "30s" # Limit resets every 30 seconds # Standard API rate limit rate_limiter: expiration: "1m" # Limit resets every minute # Long window for daily quotas rate_limiter: expiration: "24h" # Limit resets daily ``` #### `storage` (string) Storage backend for rate limit counters. - **Default**: `"memory"` - **Values**: `"memory"`, `"redis"` - **Hot-reloadable**: Yes (but existing counters are lost when switching) **Comparison**: | Feature | `"memory"` | `"redis"` | |---------|------------|-----------| | Speed | Very fast (in-process) | Fast (network call) | | Persistence | Lost on restart | Persists across restarts | | Multi-server | Independent counters | Shared counters | | Dependencies | None | Requires Redis connection | | Best for | Single server, dev/test | Multi-server, production | **Examples**: ```yaml # Memory storage (single server) rate_limiter: storage: "memory" # Redis storage (distributed) rate_limiter: storage: "redis" ``` ### Environment-Specific Configurations #### Development (`configs/config.dev.yaml`) ```yaml middleware: enable_auth: false # Optional: disable auth for easier testing enable_rate_limiter: false # Disabled by default rate_limiter: max: 1000 # High limit (avoid disruption during dev) expiration: "1m" storage: "memory" # No Redis dependency ``` **Use case**: Local development with frequent requests, no rate limiting interference #### Staging (`configs/config.staging.yaml`) ```yaml middleware: enable_auth: true enable_rate_limiter: true # Enabled to test production behavior rate_limiter: max: 1000 # Medium limit (test realistic load) expiration: "1m" storage: "memory" # Can use "redis" to test distributed limits ``` **Use case**: Pre-production testing with realistic rate limits #### Production (`configs/config.prod.yaml`) ```yaml middleware: enable_auth: true enable_rate_limiter: true # Always enabled in production rate_limiter: max: 5000 # Strict limit (prevent abuse) expiration: "1m" storage: "redis" # Distributed rate limiting ``` **Use case**: Production deployment with strict limits and distributed storage --- ## Storage Options ### Memory Storage **How it works**: Stores rate limit counters in-process memory using Fiber's built-in storage. **Pros**: - ⚡ Very fast (no network latency) - 🔧 No external dependencies - 💰 Free (no Redis costs) **Cons**: - 🔄 Counters reset on server restart - 🖥️ Each server instance has independent counters (can't enforce global limits in multi-server setup) - 📉 Memory usage grows with unique IPs **When to use**: - Single-server deployments - Development/testing environments - When rate limit precision is not critical - When Redis is unavailable or not desired **Configuration**: ```yaml rate_limiter: storage: "memory" ``` **Example scenario**: Single API server with 1000 req/min limit ``` Server 1: IP 192.168.1.1 → 950 requests → Allowed ✓ IP 192.168.1.2 → 1050 requests → 50 rejected (429) ✗ ``` ### Redis Storage **How it works**: Stores rate limit counters in Redis with automatic expiration. **Pros**: - 🌐 Distributed rate limiting (shared across all servers) - 💾 Counters persist across server restarts - 🎯 Precise global rate limit enforcement - 📊 Centralized monitoring (inspect Redis keys) **Cons**: - 🐌 Slightly slower (network call to Redis) - 💸 Requires Redis server (infrastructure cost) - 🔌 Dependency on Redis availability **When to use**: - Multi-server/load-balanced deployments - Production environments requiring strict limits - When you need consistent limits across all servers - When rate limit precision is critical **Configuration**: ```yaml rate_limiter: storage: "redis" # Ensure Redis connection is configured redis: address: "redis-prod:6379" password: "${REDIS_PASSWORD}" db: 0 ``` **Example scenario**: 3 API servers behind load balancer with 1000 req/min limit ``` Load Balancer distributes requests across servers: IP 192.168.1.1 makes 1500 requests: → 500 requests to Server 1 ✓ → 500 requests to Server 2 ✓ → 500 requests to Server 3 ✗ (global limit of 1000 reached) All servers share the same Redis counter: Redis: rate_limit:192.168.1.1 = 1000 (limit reached) ``` ### Redis Key Structure When using Redis storage, the middleware creates keys with the following pattern: ``` Key pattern: rate_limit:{ip_address} TTL: Matches expiration config ``` **Examples**: ```bash # List all rate limit keys redis-cli KEYS "rate_limit:*" # Output: # 1) "rate_limit:192.168.1.1" # 2) "rate_limit:192.168.1.2" # 3) "rate_limit:10.0.0.5" # Check counter for specific IP redis-cli GET "rate_limit:192.168.1.1" # Output: "45" (45 requests made in current window) # Check TTL (time until reset) redis-cli TTL "rate_limit:192.168.1.1" # Output: "42" (42 seconds until counter resets) ``` ### Switching Storage Backends You can switch between storage backends by changing the configuration. **Note**: Existing counters are lost when switching. **Switching from memory to Redis**: ```yaml # Before: memory storage rate_limiter: storage: "memory" # After: Redis storage (all memory counters are discarded) rate_limiter: storage: "redis" ``` **Behavior**: After config reload (within 5 seconds), new requests use Redis storage. Old memory counters are garbage collected. --- ## Code Examples ### Basic Setup (cmd/api/main.go) ```go package main import ( "github.com/break/junhong_cmp_fiber/internal/middleware" "github.com/break/junhong_cmp_fiber/pkg/config" "github.com/gofiber/fiber/v2" ) func main() { // Load configuration if err := config.LoadConfig(); err != nil { panic(err) } app := fiber.New() // Optional: Register rate limiter middleware if config.GetConfig().Middleware.EnableRateLimiter { var storage fiber.Storage = nil // Use Redis storage if configured if config.GetConfig().Middleware.RateLimiter.Storage == "redis" { storage = redisStorage // Assume redisStorage is initialized } app.Use(middleware.RateLimiter( config.GetConfig().Middleware.RateLimiter.Max, config.GetConfig().Middleware.RateLimiter.Expiration, storage, )) } // Register routes app.Get("/api/v1/users", listUsersHandler) app.Listen(":3000") } ``` ### Custom Rate Limiter (Different Limits for Different Routes) ```go // Apply different limits to different route groups // Public API - strict limit (100 req/min) publicAPI := app.Group("/api/v1/public") publicAPI.Use(middleware.RateLimiter(100, 1*time.Minute, nil)) publicAPI.Get("/data", publicDataHandler) // Internal API - relaxed limit (5000 req/min) internalAPI := app.Group("/api/v1/internal") internalAPI.Use(middleware.RateLimiter(5000, 1*time.Minute, nil)) internalAPI.Get("/metrics", internalMetricsHandler) // Admin API - very relaxed limit (10000 req/min) adminAPI := app.Group("/api/v1/admin") adminAPI.Use(middleware.RateLimiter(10000, 1*time.Minute, nil)) adminAPI.Post("/users", createUserHandler) ``` ### Bypassing Rate Limiter for Specific Routes ```go // Apply rate limiter globally app.Use(middleware.RateLimiter(100, 1*time.Minute, nil)) // But register health check BEFORE rate limiter app.Get("/health", healthHandler) // Not rate limited // Alternative: Register after but add skip logic in middleware // (requires custom middleware modification) ``` ### Testing Rate Limiter in Code ```go package main import ( "testing" "github.com/gofiber/fiber/v2" "github.com/break/junhong_cmp_fiber/internal/middleware" ) func TestRateLimiter(t *testing.T) { app := fiber.New() // Apply rate limiter: 5 requests per minute app.Use(middleware.RateLimiter(5, 1*time.Minute, nil)) app.Get("/test", func(c *fiber.Ctx) error { return c.SendString("success") }) // Make 6 requests for i := 1; i <= 6; i++ { req := httptest.NewRequest("GET", "/test", nil) resp, _ := app.Test(req) if i <= 5 { // First 5 should succeed assert.Equal(t, 200, resp.StatusCode) } else { // 6th should be rate limited assert.Equal(t, 429, resp.StatusCode) } } } ``` --- ## Testing ### Enable Rate Limiter for Testing Edit `configs/config.yaml`: ```yaml middleware: enable_rate_limiter: true # Enable rate_limiter: max: 5 # Low limit for easy testing expiration: "1m" storage: "memory" ``` Restart server or wait 5 seconds for config reload. ### Test 1: Basic Rate Limiting **Make requests until limit is reached**: ```bash # Send 10 requests rapidly for i in {1..10}; do curl -w "\nRequest $i: %{http_code}\n" \ -H "token: test-token-abc123" \ http://localhost:3000/api/v1/users sleep 0.1 done ``` **Expected output**: ``` Request 1: 200 ✓ Request 2: 200 ✓ Request 3: 200 ✓ Request 4: 200 ✓ Request 5: 200 ✓ Request 6: 429 ✗ Rate limited Request 7: 429 ✗ Rate limited Request 8: 429 ✗ Rate limited Request 9: 429 ✗ Rate limited Request 10: 429 ✗ Rate limited ``` **Rate limit response (429)**: ```json { "code": 1003, "data": null, "msg": "请求过于频繁", "timestamp": "2025-11-10T15:35:00Z" } ``` ### Test 2: Window Reset **Verify counter resets after expiration**: ```bash # Make 5 requests (hit limit) for i in {1..5}; do curl -s http://localhost:3000/api/v1/users; done # 6th request should fail curl -i http://localhost:3000/api/v1/users # Returns 429 # Wait for window to expire (1 minute) sleep 60 # Try again - should succeed curl -i http://localhost:3000/api/v1/users # Returns 200 ✓ ``` ### Test 3: Per-IP Rate Limiting **Verify different IPs have independent limits**: ```bash # IP 1: Make 5 requests (your local IP) for i in {1..5}; do curl -s http://localhost:3000/api/v1/users > /dev/null done # IP 1: 6th request should fail curl -i http://localhost:3000/api/v1/users # Returns 429 ✗ # Simulate IP 2 (requires proxy or test infrastructure) curl -H "X-Forwarded-For: 192.168.1.100" \ -i http://localhost:3000/api/v1/users # Returns 200 ✓ (separate counter for different IP) ``` ### Test 4: Redis Storage **Test Redis-based rate limiting**: ```yaml # Edit configs/config.yaml rate_limiter: storage: "redis" # Switch to Redis ``` Wait 5 seconds for config reload. ```bash # Make requests curl http://localhost:3000/api/v1/users # Check Redis for rate limit key redis-cli GET "rate_limit:127.0.0.1" # Output: "1" (one request made) # Make 4 more requests for i in {2..5}; do curl -s http://localhost:3000/api/v1/users > /dev/null; done # Check counter again redis-cli GET "rate_limit:127.0.0.1" # Output: "5" (limit reached) # Check TTL (seconds until reset) redis-cli TTL "rate_limit:127.0.0.1" # Output: "45" (45 seconds remaining) ``` ### Test 5: Concurrent Requests **Test rate limiting under concurrent load**: ```bash # Install Apache Bench (if not already installed) # macOS: brew install httpd # Linux: sudo apt-get install apache2-utils # Send 100 requests with 10 concurrent connections ab -n 100 -c 10 \ -H "token: test-token-abc123" \ http://localhost:3000/api/v1/users # Check results # With limit of 5 req/min: expect ~5 successful, ~95 rate limited ``` ### Integration Test Example See `tests/integration/ratelimit_test.go`: ```go func TestRateLimiter_LimitExceeded(t *testing.T) { app := setupRateLimiterTestApp(t, 5, 1*time.Minute) // Make 5 requests (under limit) for i := 0; i < 5; i++ { req := httptest.NewRequest("GET", "/api/v1/test", nil) resp, _ := app.Test(req) assert.Equal(t, 200, resp.StatusCode) } // 6th request (over limit) req := httptest.NewRequest("GET", "/api/v1/test", nil) resp, _ := app.Test(req) assert.Equal(t, 429, resp.StatusCode) // Verify error response var result map[string]interface{} json.NewDecoder(resp.Body).Decode(&result) assert.Equal(t, float64(1003), result["code"]) assert.Contains(t, result["msg"], "请求过于频繁") } ``` --- ## Common Usage Patterns ### Pattern 1: Tiered Rate Limits by User Type Apply different rate limits based on user tier (free, premium, enterprise): ```go // Middleware to extract user tier func tierBasedRateLimiter() fiber.Handler { return func(c *fiber.Ctx) error { userID := c.Locals(constants.ContextKeyUserID).(string) tier := getUserTier(userID) // Fetch from DB or cache var max int switch tier { case "free": max = 100 // 100 req/min case "premium": max = 1000 // 1000 req/min case "enterprise": max = 10000 // 10000 req/min default: max = 10 // Very restrictive for unknown } limiter := middleware.RateLimiter(max, 1*time.Minute, nil) return limiter(c) } } // Apply to routes app.Use(tierBasedRateLimiter()) ``` ### Pattern 2: Different Limits for Different Endpoints Apply strict limits to expensive operations, relaxed limits to cheap ones: ```go // Expensive endpoint: 10 requests/min app.Post("/api/v1/reports/generate", middleware.RateLimiter(10, 1*time.Minute, nil), generateReportHandler) // Cheap endpoint: 1000 requests/min app.Get("/api/v1/users/:id", middleware.RateLimiter(1000, 1*time.Minute, nil), getUserHandler) // Very cheap endpoint: no limit app.Get("/health", healthHandler) ``` ### Pattern 3: Burst Protection with Short Windows Prevent rapid bursts while allowing sustained traffic: ```go // Allow 10 requests per 10 seconds (burst protection) app.Use(middleware.RateLimiter(10, 10*time.Second, nil)) // This allows: // - 10 req in 1 second → OK // - 60 req in 1 minute (evenly spaced) → OK // - 100 req in 1 minute (bursty) → Some rejected ``` ### Pattern 4: Daily Quotas Implement daily request quotas for APIs: ```go // Allow 10,000 requests per day app.Use(middleware.RateLimiter(10000, 24*time.Hour, redisStorage)) // Requires Redis storage to persist across server restarts ``` ### Pattern 5: Graceful Degradation Disable rate limiting for critical internal services: ```go // Check if request is from internal network func skipRateLimitForInternal(c *fiber.Ctx) error { ip := c.IP() if isInternalIP(ip) { return c.Next() // Skip rate limiting } // Apply rate limiting for external IPs limiter := middleware.RateLimiter(100, 1*time.Minute, nil) return limiter(c) } app.Use(skipRateLimitForInternal) ``` ### Pattern 6: Combined with Authentication Apply rate limiting only after authentication: ```go // Authentication first app.Use(middleware.KeyAuth(tokenValidator, logger)) // Then rate limiting (per authenticated user) app.Use(middleware.RateLimiter(100, 1*time.Minute, nil)) // Anonymous endpoints (no auth, stricter rate limit) app.Get("/public/data", middleware.RateLimiter(10, 1*time.Minute, nil), publicDataHandler) ``` --- ## Monitoring ### Check Access Logs Rate-limited requests are logged to `logs/access.log`: ```bash # Filter for 429 status codes grep '"status":429' logs/access.log | jq . ``` **Example log entry**: ```json { "timestamp": "2025-11-10T15:35:00Z", "level": "info", "method": "GET", "path": "/api/v1/users", "status": 429, "duration_ms": 0.345, "request_id": "550e8400-e29b-41d4-a716-446655440006", "ip": "127.0.0.1", "user_agent": "curl/7.88.1", "user_id": "user-789" } ``` ### Count Rate-Limited Requests ```bash # Count 429 responses in last hour grep '"status":429' logs/access.log | \ grep "$(date -u +%Y-%m-%dT%H)" | \ wc -l # Count by IP address grep '"status":429' logs/access.log | \ jq -r '.ip' | \ sort | uniq -c | sort -rn ``` ### Monitor Redis Keys (Redis Storage Only) ```bash # Count active rate limit keys redis-cli KEYS "rate_limit:*" | wc -l # List IPs currently tracked redis-cli KEYS "rate_limit:*" # Get counter for specific IP redis-cli GET "rate_limit:192.168.1.1" # Monitor in real-time redis-cli --scan --pattern "rate_limit:*" | \ while read key; do echo "$key: $(redis-cli GET $key)" done ``` ### Metrics and Alerting **Key metrics to track**: 1. **Rate limit hit rate**: `(429 responses / total responses) * 100%` ```bash # Calculate hit rate total=$(grep -c '"status"' logs/access.log) rate_limited=$(grep -c '"status":429' logs/access.log) echo "Rate limit hit rate: $(bc <<< "scale=2; $rate_limited * 100 / $total")%" ``` 2. **Top rate-limited IPs**: Identify potential abusers ```bash grep '"status":429' logs/access.log | jq -r '.ip' | \ sort | uniq -c | sort -rn | head -10 ``` 3. **Rate limit effectiveness**: Time series of 429 responses ```bash # Group by hour grep '"status":429' logs/access.log | \ jq -r '.timestamp' | cut -d'T' -f1-2 | uniq -c ``` **Alerting thresholds**: - Alert if rate limit hit rate > 10% (too many legitimate requests being blocked) - Alert if single IP has > 100 rate-limited requests (potential abuse) - Alert if Redis storage fails (degrades to memory storage) --- ## Troubleshooting ### Problem: Rate limiter not working **Symptoms**: All requests succeed, no 429 responses even after exceeding limit **Diagnosis**: ```bash # Check if rate limiter is enabled grep "enable_rate_limiter" configs/config.yaml ``` **Solutions**: 1. Ensure `enable_rate_limiter: true` in config 2. Restart server or wait 5 seconds for config reload 3. Check logs for "Configuration reloaded" message ### Problem: Too many false positives (legitimate requests blocked) **Symptoms**: Users frequently hit rate limits during normal usage **Diagnosis**: ```bash # Check current limit grep -A3 "rate_limiter:" configs/config.yaml ``` **Solutions**: 1. Increase `max` value (e.g., from 100 to 500) 2. Increase `expiration` window (e.g., from "1m" to "5m") 3. Implement tiered limits by user type 4. Exclude internal IPs from rate limiting ### Problem: Rate limits not shared across servers **Symptoms**: In multi-server setup, each server enforces independent limits **Diagnosis**: ```bash # Check storage backend grep "storage:" configs/config.yaml ``` **Solution**: - Change `storage: "memory"` to `storage: "redis"` - Ensure Redis is properly configured and accessible from all servers ### Problem: Rate limits reset unexpectedly **Symptoms**: Counters reset before expiration window **Possible causes**: 1. **Server restart** (with memory storage) - Solution: Use Redis storage for persistence 2. **Config reload when switching storage** - Solution: Avoid switching between memory/Redis frequently 3. **Redis connection issues** (with Redis storage) - Check logs for Redis errors - Verify Redis is running: `redis-cli ping` ### Problem: Rate limiter slowing down responses **Symptoms**: Increased response latency after enabling rate limiting **Diagnosis**: ```bash # Compare response times with rate limiter on/off grep '"duration_ms"' logs/access.log | jq '.duration_ms' | \ awk '{sum+=$1; count++} END {print "Average:", sum/count, "ms"}' ``` **Solutions**: 1. If using Redis: Optimize Redis connection (increase pool size, reduce network latency) 2. Switch to memory storage if precision is not critical 3. Cache frequently accessed rate limit counters ### Problem: Redis storage not working **Symptoms**: Rate limiter falls back to memory storage, logs show Redis errors **Diagnosis**: ```bash # Check Redis connection redis-cli -h your-redis-host -p 6379 ping # Check application logs for Redis errors grep -i "redis" logs/app.log | tail -20 ``` **Solutions**: 1. Verify Redis is running and accessible 2. Check Redis credentials in config 3. Ensure Redis connection pool is properly configured 4. Check network connectivity to Redis server ### Problem: Cannot see rate limit keys in Redis **Symptoms**: `redis-cli KEYS "rate_limit:*"` returns empty **Possible causes**: 1. Rate limiter is disabled: Check `enable_rate_limiter: true` 2. Using memory storage: Check `storage: "redis"` 3. No requests have been made yet 4. Wrong Redis database: Check `redis.db` in config **Diagnosis**: ```bash # Verify storage setting grep "storage:" configs/config.yaml # Make a test request curl http://localhost:3000/api/v1/users # Check Redis again redis-cli KEYS "rate_limit:*" ``` --- ## Best Practices ### 1. Start Conservative, Then Relax Begin with stricter limits and gradually increase based on monitoring: ```yaml # Initial deployment rate_limiter: max: 100 # Conservative # After monitoring (if no issues) rate_limiter: max: 500 # Relaxed ``` ### 2. Use Redis for Production Always use Redis storage in production multi-server environments: ```yaml # Production config rate_limiter: storage: "redis" ``` ### 3. Monitor and Alert Set up monitoring and alerts for: - High rate limit hit rate (> 10%) - Suspicious IPs with many rejections - Redis connection failures ### 4. Document Rate Limits Inform API consumers about rate limits: - Include in API documentation - Return rate limit info in response headers (custom implementation) - Provide clear error messages ### 5. Combine with Authentication Apply rate limiting after authentication for better control: ```go // Good: Authenticate first, then rate limit app.Use(authMiddleware) app.Use(rateLimitMiddleware) ``` ### 6. Test Before Deploying Always test rate limits in staging before production: ```bash # Load test with rate limiting enabled ab -n 1000 -c 50 http://staging-api/endpoint ``` ### 7. Plan for Failures Ensure rate limiter fails gracefully if Redis is unavailable (already implemented): - Falls back to memory storage - Logs errors but continues serving requests --- ## Summary | Configuration | Single Server | Multi-Server | Development | Production | |---------------|---------------|--------------|-------------|------------| | `enable_rate_limiter` | Optional | Recommended | false | true | | `max` | 100-1000 | 1000-5000 | 1000+ | 100-5000 | | `expiration` | "1m" | "1m" | "1m" | "1m" | | `storage` | "memory" | "redis" | "memory" | "redis" | **Key Takeaways**: - Rate limiting protects your API from abuse - IP-based limiting ensures fair usage - Redis storage enables distributed rate limiting - Configuration is hot-reloadable (no restart needed) - Monitor 429 responses to tune limits - Always test in staging before production For more information, see: - [Quick Start Guide](../specs/001-fiber-middleware-integration/quickstart.md) - [README](../README.md) - [Implementation Plan](../specs/001-fiber-middleware-integration/plan.md)