Minimal, composable, and high-performance middleware toolkit for Gin, with conditional execution and functional chaining.
- Functional composition: Chain + Condition to precisely control execution
- Production-ready: recovery, logging, timeout, CORS, auth, RBAC, cache, rate limit
- Unified error formatting: one
ErrorFormattercontrols all middleware error responses - High performance: zero-allocation conditions, token-bucket & time-window rate limiting, sharded cache
- Clean API: unified Option/Condition pattern, easy to extend
go get github.com/simp-lee/ginx
package main import ( "time" "github.com/gin-gonic/gin" "github.com/simp-lee/ginx" ) func main() { r := gin.New() // Basic middleware stack (recommended order) r.Use(ginx.NewChain(). Use(ginx.RequestID()). // Correlation id first Use(ginx.Recovery()). // Panic protection with logging Use(ginx.Logger()). // Structured request logging Use(ginx.Timeout()). // 30s timeout protection Use(ginx.CORS(ginx.WithAllowOrigins("*"))). // CORS for development Use(ginx.RateLimit(100, 200)). // 100 RPS, 200 burst per IP Build()) r.GET("/", func(c *gin.Context) { c.JSON(200, gin.H{"message": "Hello World"}) }) r.GET("/slow", func(c *gin.Context) { // This will timeout after 30 seconds due to Timeout middleware time.Sleep(35 * time.Second) c.JSON(200, gin.H{"message": "This won't be reached"}) }) r.Run(":8080") }
// Build conditional middleware chain chain := ginx.NewChain(). Use(ginx.RequestID()). Use(ginx.Recovery()). Use(ginx.Logger()). // Apply rate limiting only to API routes When(ginx.PathHasPrefix("/api/"), ginx.RateLimit(100, 200)). // Add hourly quota for API routes When(ginx.PathHasPrefix("/api/"), ginx.RateLimitPerHour(10000)). // Apply CORS only to browser requests When(ginx.HeaderExists("Origin"), ginx.CORS(ginx.WithAllowOrigins("*"))). // Longer timeout for heavy operations When(ginx.PathHasPrefix("/api/heavy/"), ginx.Timeout(ginx.WithTimeout(60*time.Second))) r.Use(chain.Build())
type Middleware func(gin.HandlerFunc) gin.HandlerFunc type Condition func(*gin.Context) bool type Option[T any] func(*T) type ErrorHandler func(*gin.Context, error) type ErrorFormatter func(status int, message string) any
Chain provides fluent API for building middleware chains with conditional execution and error handling.
Chain methods:
NewChain()- Create new chain builderUse(m Middleware)- Add middleware unconditionallyWhen(cond Condition, m Middleware)- Add middleware if condition is trueUnless(cond Condition, m Middleware)- Add middleware if condition is falseOnError(handler ErrorHandler)- Set error handler for chain executionWithErrorFormat(f ErrorFormatter)- Set unified error response format for all middleware in the chainBuild()- Build finalgin.HandlerFunc
Note:
OnErroris invoked only whenc.Errorsis non-empty. To have errors handled by the chain-level handler, callc.Error(err)in your middleware or handlers.
Example:
chain := ginx.NewChain(). OnError(func(c *gin.Context, err error) { c.JSON(500, gin.H{"error": err.Error()}) }). Use(ginx.Recovery()). Use(ginx.Logger()). When(ginx.PathHasPrefix("/api/heavy"), ginx.Timeout(ginx.WithTimeout(60*time.Second))). Unless(ginx.PathIs("/health"), ginx.RateLimit(100, 200)) r.Use(chain.Build())
Conditions are lightweight functions of type func(*gin.Context) bool used to decide whether middleware should execute. Most conditions are zero-allocation; ContentTypeIs parses MIME types (slight cost), and PathMatches compiles regex once at condition creation.
Logic combinators:
And(conds ...Condition)- All conditions must be trueOr(conds ...Condition)- At least one condition is trueNot(cond Condition)- Condition must be false
Path conditions:
PathIs(paths ...string)- Exact path matchPathHasPrefix(prefix string)- Path starts with prefixPathHasSuffix(suffix string)- Path ends with suffixPathMatches(pattern string)- Path matches regex pattern
HTTP conditions:
MethodIs(methods ...string)- HTTP method matchesHeaderExists(key string)- Request header existsHeaderEquals(key, value string)- Header equals exact valueContentTypeIs(types ...string)- Content-Type matches (MIME parsing)
Custom conditions:
Custom(fn func(*gin.Context) bool)- Custom condition functionOnTimeout()- Request has timed outHasRequestID()- Request has a request ID set in context
RBAC conditions (require auth):
IsAuthenticated()- User is authenticatedHasPermission(service rbac.Service, resource, action string)- Combined role + user permissionsHasRolePermission(service rbac.Service, resource, action string)- Role-based permissions onlyHasUserPermission(service rbac.Service, resource, action string)- Direct user permissions only
Unified error response formatting for all middleware. Instead of configuring error responses per-middleware, a single ErrorFormatter function controls how every middleware (auth, RBAC, timeout, rate limit, recovery) renders its error response.
Usage:
Chain.WithErrorFormat(f ErrorFormatter)- Set formatter for an entire chainErrorFormat(f ErrorFormatter)- Standalone middleware (for use without Chain)
Context helpers:
SetErrorFormatter(c *gin.Context, f ErrorFormatter)- Set formatter in contextGetErrorFormatter(c *gin.Context) ErrorFormatter- Get formatter from context (nil if not set)AbortWithError(c *gin.Context, status int, message string)- Write error response using the formatter, or fall back to{"error": "<message>"}
Default behavior (no formatter set):
{"error": "request timeout"}Example with Chain:
r.Use(ginx.NewChain(). WithErrorFormat(func(status int, msg string) any { return gin.H{ "code": status, "message": msg, "success": false, } }). Use(ginx.Recovery()). Use(ginx.Logger()). Use(ginx.Timeout(ginx.WithTimeout(10 * time.Second))). Use(ginx.RateLimit(100, 200)). Build()) // All middleware errors now return: // {"code": 429, "message": "rate limit exceeded", "success": false} // {"code": 408, "message": "request timeout", "success": false} // etc.
Example with standalone middleware (no Chain):
// Works with standard Gin middleware registration r.Use(ginx.ErrorFormat(func(status int, msg string) any { return gin.H{"code": status, "message": msg} })(func(c *gin.Context) { c.Next() })) r.Use(ginx.Timeout(ginx.WithTimeout(10 * time.Second))(func(c *gin.Context) { c.Next() }))
Example per route group:
// Different formats for different API versions v1 := r.Group("/api/v1") v1.Use(ginx.NewChain(). WithErrorFormat(func(status int, msg string) any { return gin.H{"error": msg, "status": status} }). Use(ginx.RateLimit(100, 200)). Build()) v2 := r.Group("/api/v2") v2.Use(ginx.NewChain(). WithErrorFormat(func(status int, msg string) any { return gin.H{"code": status, "message": msg, "ok": false} }). Use(ginx.RateLimit(100, 200)). Build())
Notes:
- One
ErrorFormatterreplaces the need for per-middleware response options - All middleware use
AbortWithErrorinternally, so the formatter applies uniformly - When no formatter is set, the default response is
{"error": "<message>"}
Lightweight request correlation ID middleware. It sets/propagates a unique ID via header (default: X-Request-ID), stores it in Gin context, and can optionally inject the ID into Go's standard context.Context.
Usage:
RequestID(options...)- Adds/propagates request id
Options:
WithRequestIDHeader(name)- Change header name (default:X-Request-ID)WithRequestIDGenerator(func() string)- Custom ID generatorWithContextInjector(func(ctx context.Context, requestID string) context.Context)- Inject request metadata into Go context (for service/repository logging withslog.InfoContextetc.)- Default respects incoming header if present; use
WithIgnoreIncoming()to always generate a new ID
Type:
type ContextInjector func(ctx context.Context, requestID string) context.Context
Context helpers:
SetRequestID(c, id string)- Set request ID in contextGetRequestID(c) (string, bool)- Get request ID from context
Utility:
GetRequestIDFromHeader(r *http.Request, header string) string- Extract request ID from HTTP request header
Condition:
HasRequestID()- Check if request has a request ID set in context
Notes:
- Logging and Recovery middlewares automatically include
request_idif present - Place RequestID early in the chain (before Logger/Recovery) so all logs include the id
- The middleware also echoes the ID back in the response header
Example: inject into Go context (for context-aware slog):
package main import ( "context" "log/slog" "github.com/gin-gonic/gin" "github.com/simp-lee/ginx" "github.com/simp-lee/logger" ) func main() { r := gin.New() r.Use(ginx.RequestID( ginx.WithContextInjector(func(ctx context.Context, id string) context.Context { return logger.WithContextAttrs(ctx, slog.String("request_id", id)) }), )) r.GET("/users", func(c *gin.Context) { // Service/repository layers can now read request_id from c.Request.Context() // and emit it automatically with slog.InfoContext. slog.InfoContext(c.Request.Context(), "list users") c.JSON(200, gin.H{"ok": true}) }) }
Graceful panic recovery middleware with intelligent error handling and structured logging.
Usage:
Recovery(loggerOptions...)- Basic recovery with default handlerRecoveryWith(handler RecoveryHandler, loggerOptions...)- Custom recovery handler
Types:
type RecoveryHandler func(*gin.Context, any)
Features:
- Smart error detection: Distinguishes between panics and broken pipe errors
- Structured logging: Uses
github.com/simp-lee/loggerwith configurable options - Clean stack traces: Filters out recovery middleware frames and runtime panic calls
- Broken pipe handling: Special treatment for client disconnections (warns without stack trace)
- Custom responses: Configurable error response format via recovery handler
Default behavior:
- Panics: Logs error + full stack trace, returns 500 JSON response
- Broken pipes: Logs warning without stack trace, aborts connection gracefully
Example:
// Basic recovery with default handler ginx.Recovery() // Custom recovery handler with structured response ginx.RecoveryWith(func(c *gin.Context, err any) { rid, _ := ginx.GetRequestID(c) c.JSON(500, gin.H{ "error": "Internal Server Error", "request_id": rid, "timestamp": time.Now().Unix(), }) }, logger.WithLevel(slog.LevelError), logger.WithConsole(true))
Structured HTTP request logging middleware with configurable log levels and comprehensive request metadata.
Usage:
Logger(loggerOptions...)- HTTP request logger with configurable options
Features:
- Smart log levels: Automatic level based on status code (5xx=Error, 4xx=Warn, others=Info)
- Rich metadata: Method, path, query, status, latency, IP, user agent, size, protocol, referer
- Query sanitization: Automatically redacts sensitive query parameters (
token,access_token,id_token,jwt,authorization,auth,password,secret) - Error tracking: Separate error logging for gin context errors (when present)
- Structured format: Uses
github.com/simp-lee/loggerwith key-value pairs - Performance optimized: Single timer measurement, minimal allocations
- Client IP detection: Uses Gin's
ClientIP()method (supports proxy headers)
Example:
// Basic logging with default configuration ginx.Logger() // Custom log level configuration ginx.Logger(logger.WithLevel(slog.LevelDebug), logger.WithConsole(true))
Context-based request timeout middleware with buffered response handling to prevent partial responses.
Usage:
Timeout(options...)- Request timeout middleware with configurable options
Options:
WithTimeout(duration)- Set timeout duration (default: 30 seconds)WithMaxBufferSize(size int)- Set maximum response buffer size in bytes (default: 0 = unlimited)
Features:
- Atomic response handling: Buffered writer prevents partial responses during timeout
- Context cancellation: Proper request context timeout with cancellation
- Timeout detection: Sets
X-Timeout: trueheader for conditional middleware - Zero timeout support: Immediate timeout response for zero/negative durations
Helpers:
IsTimeout(c *gin.Context) bool- Check if request timed out- Condition
OnTimeout()- CheckX-Timeoutheader (pre-execution condition; not suitable for post-result timeout detection)
Important timing note:
OnTimeout()is evaluated before the wrapped middleware executes, so it cannot reliably detect timeouts that are decided later in the request lifecycle.- To detect timeout outcomes, use
IsTimeout(c)afterc.Next()in outer middleware. - Inside a timeout-protected handler, use
c.Request.Context().Done()/c.Request.Context().Err()to stop work early.
Example:
// Different timeouts for different endpoints chain := ginx.NewChain(). When(ginx.PathHasPrefix("/api/heavy"), ginx.Timeout(ginx.WithTimeout(60*time.Second))). Unless(ginx.PathIs("/health"), ginx.Timeout(ginx.WithTimeout(5*time.Second)))
Cross-Origin Resource Sharing (CORS) middleware with security-first design and proper preflight handling.
Usage:
CORS(options...)- CORS middleware with explicit origin configuration (required)CORSDefault()- Development-only helper (allows all origins)
Options:
WithAllowOrigins(origins...)- Set allowed origins (required, no default)WithAllowMethods(methods...)- Set allowed HTTP methods (default: GET, POST, PUT, DELETE, OPTIONS)WithAllowHeaders(headers...)- Set allowed request headers (default: Content-Type, Authorization, Cache-Control, X-Requested-With)WithExposeHeaders(headers...)- Set headers exposed to client (default: none)WithAllowCredentials(allow bool)- Allow credentials like cookies/auth headers (default: false)WithMaxAge(duration)- Set preflight cache duration (default: 12 hours)
Security features:
- Explicit origins required: No default origins for security
- Credentials validation: Prevents wildcard origins with credentials (runtime panic)
- Proper preflight handling: Full OPTIONS request validation
- Vary headers: Prevents proxy cache pollution
Example:
// Development: Allow all origins (use with caution) ginx.CORS(ginx.WithAllowOrigins("*")) // Production: Explicit security configuration ginx.CORS( ginx.WithAllowOrigins("https://example.com", "https://app.example.com"), ginx.WithAllowHeaders("Content-Type", "Authorization"), ginx.WithAllowCredentials(true), )
Security note: WithAllowCredentials(true) cannot be used with wildcard origin "*" (enforced at runtime).
JWT authentication middleware with secure-by-default token extraction and comprehensive context integration.
Usage:
Auth(jwtService jwt.Service, options ...Option[AuthConfig])- JWT authentication middlewareWithAuthQueryToken(true)- Explicitly enable?token=query fallback (disabled by default)
Features:
- Secure default token extraction: Uses
Authorization: Bearer <token>by default - Optional query fallback:
?token=<token>is only enabled withWithAuthQueryToken(true) - Automatic context population: Sets user ID, roles, and token metadata in gin context
- Type-safe context keys: Uses typed context keys to prevent conflicts
- Validation & parsing: Uses
jwtService.ValidateAndParse()for comprehensive token validation
Context helpers (getters):
GetUserID(c) (string, bool)- Get authenticated user IDGetUserRoles(c) ([]string, bool)- Get user roles from tokenGetTokenID(c) (string, bool)- Get JWT token IDGetTokenExpiresAt(c) (time.Time, bool)- Get token expiration timeGetTokenIssuedAt(c) (time.Time, bool)- Get token issued timeGetUserIDOrAbort(c) (string, bool)- Get user ID or abort with 401 if not authenticated
Context helpers (setters):
SetUserID(c, userID string)- Set user ID in contextSetUserRoles(c, roles []string)- Set user roles in contextSetTokenID(c, tokenID string)- Set token ID in contextSetTokenExpiresAt(c, expiresAt time.Time)- Set token expirationSetTokenIssuedAt(c, issuedAt time.Time)- Set token issued time
Example:
jwtService, _ := jwt.New("secret-key", jwt.WithLeeway(5*time.Minute)) // Protect API routes with JWT r.Use(ginx.NewChain(). When(ginx.PathHasPrefix("/api/"), ginx.Auth(jwtService)). Build())
Role-based access control middleware with fine-grained permission checking and condition support.
Usage:
- Middlewares (require authentication):
RequirePermission(service rbac.Service, resource, action string)- Check combined role + user permissionsRequireRolePermission(service rbac.Service, resource, action string)- Check role-based permissions onlyRequireUserPermission(service rbac.Service, resource, action string)- Check direct user permissions only
Features:
- Three permission models: Combined, role-only, and user-only permission checking
- Automatic authentication check: Uses
GetUserIDOrAbort()for user validation - Detailed error responses: Distinguishes between permission check failures (500) and access denied (403)
- Integration with Auth: Seamlessly works with JWT authentication middleware
Conditions (for conditional middleware):
IsAuthenticated()- Check if user is authenticated (no service required)HasPermission(service rbac.Service, resource, action string)- Check combined permissionsHasRolePermission(service rbac.Service, resource, action string)- Check role permissionsHasUserPermission(service rbac.Service, resource, action string)- Check user permissions
Error handling:
- 500 Internal Server Error: Permission check failed (service error)
- 403 Forbidden: Permission denied (access not allowed)
- 401 Unauthorized: User not authenticated (handled by
GetUserIDOrAbort)
Example:
rbacService, _ := rbac.New() // Require admin permissions for admin routes r.Use(ginx.NewChain(). When(ginx.PathHasPrefix("/api/admin/"), ginx.RequireRolePermission(rbacService, "admin", "access")). Build())
HTTP-compliant response caching middleware with intelligent cache control and group support.
Usage:
Cache(cache shardedcache.CacheInterface)- Cache all cacheable responses (default group)CacheWithGroup(cache shardedcache.CacheInterface, groupName string)- Cache with group prefix for isolationCacheWithOptions(cache shardedcache.CacheInterface, opts ...CacheOption)- Cache with custom options (default group)CacheWithGroupOptions(cache shardedcache.CacheInterface, groupName string, opts ...CacheOption)- Grouped cache with custom options
Features:
- HTTP-compliant caching: Respects
Cache-Control: no-store/private/no-cache/must-revalidate/max-age=0directives - Smart exclusions: Automatically excludes responses with
Set-Cookieheaders to prevent user data leakage - Auth/session-safe default: Skips caching when request contains
AuthorizationorCookieheader - Range-safe: Bypasses cache for
Rangerequests andContent-Rangeresponses (partial content) - 2xx-only caching: Only caches successful responses (200-299, excluding 206 Partial Content)
- GET & HEAD support: Caches both GET and HEAD responses; only body is omitted on HEAD replay
- Safer default cache keys: Generated from HTTP method + host + path + query (
METHOD|HOST|PATH?QUERY) - Content negotiation safety: Default keys include
Accept-Encodingvariant when present to avoid representation mix-ups - Configurable key strategy:
WithCacheKeyFunc(func(*gin.Context) string)supports custom variant/context dimensions - Configurable vary dimensions:
WithCacheVaryHeaders(headers...)extends/overrides header dimensions used by default key generation - Response reconstruction: Preserves status code/body and replays full response headers including multi-value headers (
Link,Vary, etc.); responses withSet-Cookieare not cached - Group isolation: Optional grouping for cache namespace separation
Cache key format:
GET|api.example.com|/api/users // No query parameters
GET|api.example.com|/api/search?q=test&limit=10 // With query parameters
GET|api.example.com|/api/users|h:Accept-Encoding=gzip // Content-encoding variant
Example:
cache := shardedcache.NewCache(shardedcache.Options{ MaxSize: 1000, DefaultExpiration: 5 * time.Minute, }) // Cache GET requests with version-specific grouping r.Use(ginx.NewChain(). When(ginx.And(ginx.MethodIs("GET"), ginx.PathHasPrefix("/api/v1/")), ginx.CacheWithGroup(cache, "api-v1")). When(ginx.And(ginx.MethodIs("GET"), ginx.PathHasPrefix("/api/v2/")), ginx.CacheWithGroup(cache, "api-v2")). When(ginx.And( ginx.MethodIs("GET"), ginx.PathHasPrefix("/api/"), ginx.Not(ginx.Or( ginx.PathHasPrefix("/api/v1/"), ginx.PathHasPrefix("/api/v2/"), )), ), ginx.Cache(cache)). Build())
High-performance rate limiting middleware supporting both token bucket (RPS) and time-window strategies (per minute/hour/day).
Smooth rate limiting using token bucket algorithm for requests per second.
Usage:
RateLimit(rps int, burst int, opts ...RateOption)- Token bucket rate limiting with configurable options
Key generation options:
WithIP()- IP-based rate limiting (default behavior)WithUser()- Per-user rate limiting using authenticated context (user_id)WithTrustedUserHeader(name)- Optional trusted header fallback for gateway-injected identityWithPath()- Per-path rate limiting (different limits per endpoint)WithKeyFunc(keyFunc func(*gin.Context) string)- Custom key generation function
Security note:
WithUser()does not trust client-provided headers.- Use
WithTrustedUserHeader(name)only when the header is set by trusted infrastructure (API gateway/auth proxy) and cannot be spoofed by clients.
Control options:
WithSkipFunc(skipFunc func(*gin.Context) bool)- Skip certain requestsWithWait(timeout time.Duration)- Wait for tokens instead of immediate rejectionWithDynamicLimits(getLimits func(key string) (rps, burst int))- Dynamic per-key limitsWithStore(store RateLimitStore)- Custom storage backend (default: shared memory; see Custom Storage Backends)
Header options:
WithoutRateLimitHeaders()- DisableX-RateLimit-*headersWithoutRetryAfterHeader()- DisableRetry-Afterheader (enabled by default)
Features:
- Token bucket algorithm: Smooth rate limiting using
golang.org/x/time/rate - Multiple key strategies: IP, user ID, path, or custom key generation
- Dynamic limits: Per-key rate limits based on user plan, endpoint type, etc.
- Wait middleware: Traffic smoothing by waiting for available tokens
- HTTP compliance: Standard
X-RateLimit-*andRetry-Afterheaders - Thread-safe: Designed for high-concurrency environments
HTTP headers:
X-RateLimit-Limit: 100 // Requests per second
X-RateLimit-Remaining: 85 // Available tokens
X-RateLimit-Reset: 1234567890 // Token bucket full reset time (Unix timestamp)
Retry-After: 3 // Seconds to wait (429 responses only)
Note:
- In unlimited mode (both
rpsandburstare<= 0), noX-RateLimit-*headers are returned.
Example:
// Basic IP-based rate limiting: 100 rps, burst 200 r.Use(ginx.RateLimit(100, 200)) // Dynamic per-user limits with wait mode r.Use(ginx.RateLimit(0, 0, ginx.WithUser(), ginx.WithWait(2*time.Second), ginx.WithDynamicLimits(func(key string) (int, int) { if strings.HasPrefix(key, "user:premium_") { return 100, 200 // Premium users } return 10, 20 // Regular users }), ))
Fixed window rate limiting for precise quota management.
Usage:
RateLimitPerMinute(limit int, opts ...RateOption)- Maximum requests per minuteRateLimitPerHour(limit int, opts ...RateOption)- Maximum requests per hourRateLimitPerDay(limit int, opts ...RateOption)- Maximum requests per day
Supported options:
WithIP()- IP-based limiting (default)WithUser()- Per-user limitingWithPath()- Per-path limitingWithKeyFunc()- Custom key functionWithSkipFunc()- Skip certain requestsWithWindowStore(store WindowCounterStore)- Custom storage backend (see Custom Storage Backends)WithDynamicWindowLimits(getLimit func(key string) int)- Dynamic per-key limitsWithoutRateLimitHeaders()- Disable headersWithoutRetryAfterHeader()- Disable Retry-After header
Note: Time-window rate limiting does not support WithWait() option.
Features:
- Fixed window algorithm: Precise quota control within time windows
- Window reset times:
- Minute: At 0 seconds of each minute (e.g., 14:35:00)
- Hour: At 0 minutes of each hour (e.g., 14:00:00)
- Day: At midnight each day (00:00:00)
- Independent counters: Each window maintains its own counter
- Automatic cleanup: Expired counters are automatically removed
- Thread-safe: Designed for high-concurrency environments
HTTP headers:
X-RateLimit-Limit-Minute: 60 // Maximum per minute
X-RateLimit-Remaining-Minute: 45 // Remaining this minute
X-RateLimit-Reset-Minute: 1234567890 // Window reset time (Unix timestamp)
Retry-After: 15 // Seconds until window resets (429 only)
Example:
// Limit to 60 requests per minute r.Use(ginx.RateLimitPerMinute(60)) // Limit to 1000 requests per hour per user r.Use(ginx.RateLimitPerHour(1000, ginx.WithUser())) // Limit to 10000 requests per day r.Use(ginx.RateLimitPerDay(10000)) // Dynamic per-user limits based on user tier r.Use(ginx.RateLimitPerHour(0, // Base limit ignored when using dynamic limits ginx.WithUser(), ginx.WithDynamicWindowLimits(func(key string) int { if strings.Contains(key, "user:premium_") { return 100000 // Premium: 100k per hour } if strings.Contains(key, "user:pro_") { return 10000 // Pro: 10k per hour } return 1000 // Free: 1k per hour }), ))
Combine RPS and time-window rate limiting for multi-layer protection.
Usage:
// Two-layer protection: RPS + hourly quota r.Use(ginx.NewChain(). Use(ginx.RateLimit(10, 20)). // Prevent instant spikes Use(ginx.RateLimitPerHour(1000)). // Hourly quota management Build()) // Three-layer protection: RPS + hourly + daily quota r.Use(ginx.NewChain(). Use(ginx.RateLimit(5, 10)). // Instant protection Use(ginx.RateLimitPerHour(1000)). // Hourly quota Use(ginx.RateLimitPerDay(10000)). // Daily quota Build())
Use cases:
- Public APIs: Moderate RPS + daily quota
- Premium users: High RPS + generous hourly/daily quota
- Sensitive operations: Strict RPS + low hourly/daily quota
- Heavy endpoints: Low RPS + low hourly quota
Response headers when combined:
X-RateLimit-Limit: 10
X-RateLimit-Remaining: 9
X-RateLimit-Reset: 1700000001 // Unix timestamp when the bucket refills
X-RateLimit-Limit-Hour: 1000
X-RateLimit-Remaining-Hour: 850
X-RateLimit-Reset-Hour: 1700003600 // Unix timestamp when the hourly window resets
Retry-After: 30
Resource management:
- Built-in shared memory stores with automatic cleanup
- Call
ginx.CleanupRateLimiters()on application shutdown for comprehensive cleanup
For advanced scenarios (e.g. Redis-backed rate limiting), implement the exported store interfaces:
Token bucket store:
// RateLimitStore defines the interface for storing and managing rate limiters. type RateLimitStore interface { Get(key string) (*rate.Limiter, bool) Set(key string, limiter *rate.Limiter) Delete(key string) Clear() Close() error }
Time-window counter store:
// WindowCounterStore defines the interface for storing time-window based counters. type WindowCounterStore interface { Increment(key string, window time.Time) (int64, error) IncrementWithinLimit(key string, window time.Time, limit int64) (count int64, allowed bool, err error) Get(key string, window time.Time) (int64, error) Clear() Close() error }
Built-in constructors:
NewMemoryLimiterStore(maxIdle time.Duration) RateLimitStore- In-memory token bucket store with auto-cleanupNewMemoryWindowCounterStore(maxIdle time.Duration) WindowCounterStore- In-memory window counter store with auto-cleanup
Example:
// Use custom store for token bucket rate limiting customStore := ginx.NewMemoryLimiterStore(30 * time.Minute) r.Use(ginx.RateLimit(100, 200, ginx.WithStore(customStore))) // Use custom store for window rate limiting windowStore := ginx.NewMemoryWindowCounterStore(2 * time.Hour) r.Use(ginx.RateLimitPerHour(1000, ginx.WithWindowStore(windowStore)))
package main import ( "time" "github.com/gin-gonic/gin" "github.com/simp-lee/ginx" "github.com/simp-lee/jwt" "github.com/simp-lee/rbac" shardedcache "github.com/simp-lee/cache" ) func main() { r := gin.New() // Setup services with proper configuration jwtService, _ := jwt.New("your-super-secret-key-here", jwt.WithLeeway(5*time.Minute), jwt.WithIssuer("ginx-app"), jwt.WithMaxTokenLifetime(24*time.Hour), ) rbacService, _ := rbac.New() // Default memory storage cache := shardedcache.NewCache(shardedcache.Options{ MaxSize: 1000, DefaultExpiration: 5 * time.Minute, ShardCount: 16, // Concurrent access optimization CleanupInterval: 1 * time.Minute, // Automatic cleanup }) // Production middleware chain with conditional logic isAPIPath := ginx.PathHasPrefix("/api/") isPublicPath := ginx.Or(ginx.PathIs("/api/login", "/api/register")) isHealthPath := ginx.Or(ginx.PathIs("/health", "/metrics")) isAdminPath := ginx.PathHasPrefix("/api/admin/") r.Use(ginx.NewChain(). OnError(func(c *gin.Context, err error) { c.JSON(500, gin.H{"error": "Internal server error"}) }). // Base middleware for all requests Use(ginx.Recovery()). Use(ginx.Logger()). // CORS for web clients (production origins) Use(ginx.CORS( ginx.WithAllowOrigins("https://yourdomain.com", "https://app.yourdomain.com"), ginx.WithAllowMethods("GET", "POST", "PUT", "DELETE", "OPTIONS"), ginx.WithAllowHeaders("Content-Type", "Authorization"), ginx.WithAllowCredentials(true), )). // Different timeouts for different endpoint types When(ginx.PathHasPrefix("/api/heavy/"), ginx.Timeout(ginx.WithTimeout(60*time.Second))). Unless(isHealthPath, ginx.Timeout(ginx.WithTimeout(30*time.Second))). // Multi-layer rate limiting (skip health checks) When(ginx.Not(isHealthPath), ginx.RateLimit(100, 200)). When(ginx.Not(isHealthPath), ginx.RateLimitPerHour(10000)). When(ginx.Not(isHealthPath), ginx.RateLimitPerDay(100000)). // JWT authentication for API routes (skip public endpoints) When(ginx.And(isAPIPath, ginx.Not(isPublicPath)), ginx.Auth(jwtService)). // Admin area protection When(isAdminPath, ginx.RequirePermission(rbacService, "admin", "access")). // Cache only public GET API responses (requests with Authorization are skipped by default) When(ginx.And(ginx.MethodIs("GET"), isAPIPath, ginx.Not(ginx.IsAuthenticated())), ginx.Cache(cache)). Build()) // Routes r.GET("/health", func(c *gin.Context) { c.JSON(200, gin.H{"status": "ok"}) }) api := r.Group("/api") { api.POST("/login", handleLogin) api.GET("/users", handleGetUsers) // Cached api.POST("/users", handleCreateUser) // Not cached admin := api.Group("/admin") { admin.GET("/stats", handleAdminStats) // Requires admin role admin.DELETE("/users/:id", handleDeleteUser) // Requires admin role } } r.Run(":8080") }
func setupMicroservice() gin.HandlerFunc { return ginx.NewChain(). Use(ginx.Recovery()). Use(ginx.Logger()). Use(ginx.Timeout(ginx.WithTimeout(10*time.Second))). // Different rate limits for different client types When(ginx.PathHasPrefix("/internal/"), ginx.RateLimit(1000, 2000)). // High limits for internal services When(ginx.PathHasPrefix("/api/public/"), ginx.RateLimit(10, 20)). // Low RPS for public API When(ginx.PathHasPrefix("/api/public/"), ginx.RateLimitPerHour(1000)). // Hourly quota for public API When(ginx.And( ginx.PathHasPrefix("/api/"), ginx.HeaderExists("X-API-Key"), ), ginx.RateLimit(100, 200)). // Medium limits for API key users Build() }
// Per-tenant RPS rate limiting with dynamic limits based on subscription plan r.Use(ginx.RateLimit(0, 0, ginx.WithUser(), // Rate limit per user ginx.WithDynamicLimits(func(key string) (int, int) { // key format: "user:<id>" if strings.Contains(key, "user:premium_") { return 1000, 2000 // Premium users: 1000 RPS, burst 2000 } if strings.Contains(key, "user:pro_") { return 100, 200 // Pro users: 100 RPS, burst 200 } return 10, 20 // Free users: 10 RPS, burst 20 }), )) // Per-user hourly quotas based on subscription plan r.Use(ginx.RateLimitPerHour(0, ginx.WithUser(), ginx.WithDynamicWindowLimits(func(key string) int { if strings.Contains(key, "user:premium_") { return 100000 // Premium: 100k per hour } if strings.Contains(key, "user:pro_") { return 10000 // Pro: 10k per hour } return 1000 // Free: 1k per hour }), )) // Feature-based conditional access control isAnalyticsPath := ginx.PathHasPrefix("/api/analytics/") isBillingPath := ginx.PathHasPrefix("/api/billing/") isReportingPath := ginx.PathHasPrefix("/api/reporting/") r.Use(ginx.NewChain(). // Analytics requires analytics permission When(isAnalyticsPath, ginx.RequireRolePermission(rbacService, "analytics", "read")). // Billing requires billing access When(isBillingPath, ginx.RequireRolePermission(rbacService, "billing", "access")). // Advanced reporting for premium users only When(isReportingPath, ginx.RequireRolePermission(rbacService, "reporting", "generate")). // Cache expensive analytics queries When(ginx.And(isAnalyticsPath, ginx.MethodIs("GET")), ginx.CacheWithGroup(cache, "analytics")). Build())
// Real-world caching strategy with multiple cache groups and conditions func setupAdvancedCaching(r *gin.Engine, cache shardedcache.CacheInterface) { // Define path conditions for clarity isAPIPath := ginx.PathHasPrefix("/api/") isPublicData := ginx.PathHasPrefix("/public/") isUserSpecific := ginx.PathHasPrefix("/api/users/") isAdminData := ginx.PathHasPrefix("/admin/") // Advanced caching chain with different strategies r.Use(ginx.NewChain(). // Cache public data aggressively (separate group for easy management) When(ginx.And(ginx.MethodIs("GET"), isPublicData), ginx.CacheWithGroup(cache, "public")). // Cache API GET requests but exclude health/status endpoints When(ginx.And( ginx.MethodIs("GET"), isAPIPath, ginx.Not(ginx.Or(ginx.PathIs("/api/health", "/api/status"))), ), ginx.CacheWithGroup(cache, "api")). // User-specific data with separate group (privacy isolation) When(ginx.And(ginx.MethodIs("GET"), isUserSpecific), ginx.CacheWithGroup(cache, "users")). // Never cache admin data (add no-cache headers via custom middleware) When(isAdminData, func(next gin.HandlerFunc) gin.HandlerFunc { return func(c *gin.Context) { c.Header("Cache-Control", "no-store, no-cache, must-revalidate") next(c) } }). Build()) }
// Multi-layer rate limiting for different scenarios func setupRateLimiting(r *gin.Engine) { // Example 1: Public API with burst + quota protection publicAPI := r.Group("/api/public") publicAPI.Use(ginx.NewChain(). Use(ginx.RateLimit(10, 20)). // Prevent instant spikes Use(ginx.RateLimitPerHour(1000)). // Hourly quota Use(ginx.RateLimitPerDay(10000)). // Daily quota Build()) // Example 2: Authenticated API with per-user limits authAPI := r.Group("/api/v1") authAPI.Use(ginx.NewChain(). Use(ginx.RateLimit(50, 100, ginx.WithUser())). // Per-user RPS Use(ginx.RateLimitPerHour(5000, ginx.WithUser())). // Per-user hourly quota Build()) // Example 3: Heavy operations with strict limits r.POST("/api/heavy-task", ginx.NewChain(). Use(ginx.RateLimit(1, 1)). // Only 1 request per second Use(ginx.RateLimitPerHour(10)). // Max 10 per hour Use(ginx.RateLimitPerDay(50)). // Max 50 per day Build(), handleHeavyTask) // Example 4: Path-based rate limiting for different endpoints r.Use(ginx.NewChain(). When(ginx.PathHasPrefix("/api/search/"), ginx.NewChain(). Use(ginx.RateLimit(5, 10, ginx.WithPath())). Use(ginx.RateLimitPerMinute(50, ginx.WithPath())). Build()). When(ginx.PathHasPrefix("/api/login"), ginx.NewChain(). Use(ginx.RateLimit(1, 2, ginx.WithIP())). Use(ginx.RateLimitPerHour(5, ginx.WithIP())). Build()). Build()) // Example 5: Dynamic per-user tier-based rate limiting r.Use(ginx.NewChain(). // RPS based on user tier (supports dynamic limits) Use(ginx.RateLimit(0, 0, ginx.WithUser(), ginx.WithDynamicLimits(getUserRPSLimits))). // Hourly quota based on user tier Use(ginx.RateLimitPerHour(0, ginx.WithUser(), ginx.WithDynamicWindowLimits(getUserHourlyLimits))). // Daily quota based on user tier Use(ginx.RateLimitPerDay(0, ginx.WithUser(), ginx.WithDynamicWindowLimits(getUserDailyLimits))). Build()) } func getUserRPSLimits(key string) (int, int) { // Extract user tier from key if strings.Contains(key, "user:premium_") { return 100, 200 // Premium: 100 RPS, burst 200 } if strings.Contains(key, "user:pro_") { return 50, 100 // Pro: 50 RPS, burst 100 } return 10, 20 // Free: 10 RPS, burst 20 } func getUserHourlyLimits(key string) int { if strings.Contains(key, "user:premium_") { return 50000 // Premium: 50k per hour } if strings.Contains(key, "user:pro_") { return 5000 // Pro: 5k per hour } return 500 // Free: 500 per hour } func getUserDailyLimits(key string) int { if strings.Contains(key, "user:premium_") { return 1000000 // Premium: 1M per day } if strings.Contains(key, "user:pro_") { return 100000 // Pro: 100k per day } return 10000 // Free: 10k per day }
Ginx exports lightweight test utilities (in test_helpers.go) so downstream packages can unit-test their middleware and handlers without boilerplate.
Context & handler helpers:
TestContext(method, path string, headers map[string]string) (*gin.Context, *httptest.ResponseRecorder)- Create a ready-to-usegin.Contextin test mode with custom method, path, headers, and a validRemoteAddrTestMiddleware(name string, executed *[]string) Middleware- Create a middleware that records its execution by appendingnameto the sliceTestHandler(executed *[]string) gin.HandlerFunc- Create a handler that records execution by appending"handler"to the slice
Rate limit helpers:
SetupRateLimitTest(t testing.TB)- Register automatic cleanup of global rate limiter state viat.Cleanup; call at the start of any test that exercises rate limiting
Assertion helpers:
AssertContains(slice []string, item string) bool- Check if a string slice contains an elementAssertEqual(expected, actual interface{}) bool- Check equality of two valuesAssertSliceEqual(expected, actual []string) bool- Check equality of two string slices
Example:
package myapp import ( "testing" "github.com/simp-lee/ginx" ) func TestMyMiddleware(t *testing.T) { ginx.SetupRateLimitTest(t) // automatic rate-limiter cleanup var executed []string c, w := ginx.TestContext("GET", "/api/test", map[string]string{ "Authorization": "Bearer token123", }) // Build a chain: your middleware + recording handler chain := ginx.NewChain(). Use(ginx.TestMiddleware("before", &executed)). Use(MyCustomMiddleware()). Build() chain(c) if w.Code != 200 { t.Errorf("expected 200, got %d", w.Code) } if !ginx.AssertContains(executed, "before") { t.Error("expected 'before' middleware to execute") } }
- Conditions efficiency: Most conditions are zero-allocation;
ContentTypeIsparses MIME (small overhead), andPathMatchescompiles the regex at condition creation time (not per request). - Functional composition: Minimal middleware chain overhead with conditional execution
- Sharded caching: Reduced lock contention for high-concurrency scenarios
- Rate limiting: Token bucket for smooth RPS control, fixed window counters for quota management, both with automatic memory cleanup
- Compiled patterns: Cached regex for
PathMatches()condition
Core dependencies:
github.com/gin-gonic/ginv1.11.0 - Web frameworkgolang.org/x/timev0.14.0 - Rate limiting implementation
Feature dependencies (pulled automatically):
github.com/simp-lee/jwt- JWT authentication (Auth middleware)github.com/simp-lee/rbac- Role-based access control (RBAC middleware)github.com/simp-lee/logger- Structured logging (Logger/Recovery middleware)github.com/simp-lee/cache- Response caching (Cache middleware)
Testing:
github.com/stretchr/testifyv1.11.1 - Test assertions
MIT