19 KiB
Data Model: Fiber Middleware Integration
Feature: 001-fiber-middleware-integration
Date: 2025-11-10
Phase: 1 - Design & Contracts
Overview
This document defines the data structures, entities, and models used in the middleware integration feature. All structures follow Go idiomatic design principles with simple, flat structures and no Java-style patterns.
1. Configuration Model
Config Structure
File: pkg/config/config.go
// Config represents the application configuration
type Config struct {
Server ServerConfig `mapstructure:"server"`
Redis RedisConfig `mapstructure:"redis"`
Logging LoggingConfig `mapstructure:"logging"`
Middleware MiddlewareConfig `mapstructure:"middleware"`
}
// ServerConfig contains HTTP server settings
type ServerConfig struct {
Address string `mapstructure:"address"` // e.g., ":3000"
ReadTimeout time.Duration `mapstructure:"read_timeout"` // e.g., "10s"
WriteTimeout time.Duration `mapstructure:"write_timeout"` // e.g., "10s"
ShutdownTimeout time.Duration `mapstructure:"shutdown_timeout"` // e.g., "30s"
Prefork bool `mapstructure:"prefork"` // Multi-process mode
}
// RedisConfig contains Redis connection settings
type RedisConfig struct {
Address string `mapstructure:"address"` // e.g., "localhost:6379"
Password string `mapstructure:"password"` // Leave empty if no auth
DB int `mapstructure:"db"` // Database number (0-15)
PoolSize int `mapstructure:"pool_size"` // Max connections
MinIdleConns int `mapstructure:"min_idle_conns"` // Keep-alive connections
DialTimeout time.Duration `mapstructure:"dial_timeout"` // e.g., "5s"
ReadTimeout time.Duration `mapstructure:"read_timeout"` // e.g., "3s"
WriteTimeout time.Duration `mapstructure:"write_timeout"` // e.g., "3s"
}
// LoggingConfig contains logging settings
type LoggingConfig struct {
Level string `mapstructure:"level"` // debug, info, warn, error
Development bool `mapstructure:"development"` // Enable dev mode (pretty print)
AppLog LogRotationConfig `mapstructure:"app_log"` // Application log settings
AccessLog LogRotationConfig `mapstructure:"access_log"` // HTTP access log settings
}
// LogRotationConfig contains log rotation settings for Lumberjack
type LogRotationConfig struct {
Filename string `mapstructure:"filename"` // Log file path
MaxSize int `mapstructure:"max_size"` // Max size in MB before rotation
MaxBackups int `mapstructure:"max_backups"` // Max number of old files to keep
MaxAge int `mapstructure:"max_age"` // Max days to retain old files
Compress bool `mapstructure:"compress"` // Compress rotated files
}
// MiddlewareConfig contains middleware settings
type MiddlewareConfig struct {
EnableAuth bool `mapstructure:"enable_auth"` // Enable keyauth middleware
EnableRateLimiter bool `mapstructure:"enable_rate_limiter"` // Enable limiter (default: false)
RateLimiter RateLimiterConfig `mapstructure:"rate_limiter"` // Rate limiter settings
}
// RateLimiterConfig contains rate limiter settings
type RateLimiterConfig struct {
Max int `mapstructure:"max"` // Max requests per window
Expiration time.Duration `mapstructure:"expiration"` // Time window (e.g., "1m")
Storage string `mapstructure:"storage"` // "memory" or "redis"
}
// Validate validates configuration values
func (c *Config) Validate() error {
if c.Server.Address == "" {
return errors.New("server address cannot be empty")
}
if c.Server.ReadTimeout <= 0 {
return errors.New("server read timeout must be positive")
}
if c.Redis.Address == "" {
return errors.New("redis address cannot be empty")
}
if c.Redis.PoolSize <= 0 {
return errors.New("redis pool size must be positive")
}
if c.Logging.AppLog.Filename == "" {
return errors.New("app log filename cannot be empty")
}
if c.Logging.AccessLog.Filename == "" {
return errors.New("access log filename cannot be empty")
}
if c.Middleware.RateLimiter.Max <= 0 {
c.Middleware.RateLimiter.Max = 100 // Default
}
return nil
}
Example YAML Configuration (configs/config.yaml):
server:
address: ":3000"
read_timeout: "10s"
write_timeout: "10s"
shutdown_timeout: "30s"
prefork: false
redis:
address: "localhost:6379"
password: ""
db: 0
pool_size: 10
min_idle_conns: 5
dial_timeout: "5s"
read_timeout: "3s"
write_timeout: "3s"
logging:
level: "info"
development: false
app_log:
filename: "logs/app.log"
max_size: 100 # MB
max_backups: 30
max_age: 30 # days
compress: true
access_log:
filename: "logs/access.log"
max_size: 500 # MB
max_backups: 90
max_age: 90 # days
compress: true
middleware:
enable_auth: true
enable_rate_limiter: false # Disabled by default
rate_limiter:
max: 100 # requests
expiration: "1m" # per minute
storage: "memory" # or "redis"
Validation Rules:
- All required fields must be non-empty
- Timeouts must be positive durations
- Pool sizes must be positive integers
- Log filenames must be valid paths
- Rate limiter max must be positive (defaults to 100)
2. Authentication Model
AuthToken Entity
Storage: Redis key-value pair
Key Format: auth:token:{token_string} (generated via constants.RedisAuthTokenKey())
Value Format: Plain string containing user ID
TTL: Managed by Redis (set when token is created)
// Token validation doesn't use a struct - just Redis key-value
// Key: "auth:token:abc123def456"
// Value: "user-789"
// TTL: 3600 seconds (1 hour)
Redis Operations:
// Store token (example - not part of this feature)
rdb.Set(ctx, constants.RedisAuthTokenKey(token), userID, 1*time.Hour)
// Validate token (this feature)
userID, err := rdb.Get(ctx, constants.RedisAuthTokenKey(token)).Result()
if err == redis.Nil {
// Token not found or expired
}
// Delete token (example - logout feature)
rdb.Del(ctx, constants.RedisAuthTokenKey(token))
Key Generation Function (pkg/constants/redis.go):
// RedisAuthTokenKey generates Redis key for authentication tokens
func RedisAuthTokenKey(token string) string {
return fmt.Sprintf("auth:token:%s", token)
}
Validation Rules:
- Token must exist as Redis key
- Redis must be available (fail closed if not)
- Token value must be non-empty user ID
- TTL is checked automatically by Redis (expired keys return
redis.Nil)
3. Request Context Model
RequestContext Structure
File: pkg/middleware/context.go (or stored in Fiber's c.Locals())
// Request context is stored in Fiber's Locals, not a struct
// Access via: c.Locals("key")
// Request ID (set by requestid middleware)
requestID := c.Locals("requestid").(string) // UUID v4 string
// User ID (set by keyauth middleware after validation)
userID, ok := c.Locals("user_id").(string)
if !ok {
// Not authenticated
}
// Start time (for duration calculation)
startTime := c.Locals("start_time").(time.Time)
Context Keys (constants in pkg/constants/constants.go):
const (
// Context keys for Fiber Locals
ContextKeyRequestID = "requestid"
ContextKeyUserID = "user_id"
ContextKeyStartTime = "start_time"
)
Lifecycle:
- requestid middleware: Sets
requestidin Locals (UUID v4) - logger middleware: Sets
start_timein Locals - keyauth middleware: Sets
user_idin Locals (after validation) - handler: Accesses context values from Locals
- logger middleware (after handler): Calculates duration and logs
4. Log Entry Models
Application Log Entry
Format: JSON
Output: logs/app.log
Logger: Zap (appLogger instance)
{
"timestamp": "2025-11-10T15:30:45.123Z",
"level": "info",
"logger": "service.user",
"caller": "user/service.go:42",
"message": "User created successfully",
"request_id": "550e8400-e29b-41d4-a716-446655440000",
"user_id": "user-789",
"username": "john_doe",
"ip": "192.168.1.100"
}
Fields:
timestamp: ISO 8601 format (RFC3339)level: debug, info, warn, errorlogger: Logger name (optional, for structured logging)caller: Source file and line numbermessage: Log messagerequest_id: Request correlation ID (if available)user_id: Authenticated user ID (if available)- Custom fields: Any additional context-specific fields
Zap Usage:
appLogger.Info("User created successfully",
zap.String("request_id", requestID),
zap.String("user_id", userID),
zap.String("username", username),
zap.String("ip", ip),
)
Access Log Entry
Format: JSON
Output: logs/access.log
Logger: Zap (accessLogger instance)
{
"timestamp": "2025-11-10T15:30:45.123Z",
"level": "info",
"method": "POST",
"path": "/api/v1/users",
"status": 200,
"duration_ms": 45.234,
"request_id": "550e8400-e29b-41d4-a716-446655440000",
"ip": "192.168.1.100",
"user_agent": "Mozilla/5.0...",
"user_id": "user-789"
}
Fields:
timestamp: ISO 8601 format (RFC3339)level: Always "info" for access logsmethod: HTTP method (GET, POST, PUT, DELETE)path: Request pathstatus: HTTP status codeduration_ms: Request duration in millisecondsrequest_id: Request correlation IDip: Client IP addressuser_agent: User-Agent headeruser_id: Authenticated user ID (if available, empty if not)
Zap Usage:
accessLogger.Info("",
zap.String("method", c.Method()),
zap.String("path", c.Path()),
zap.Int("status", c.Response().StatusCode()),
zap.Float64("duration_ms", duration.Seconds()*1000),
zap.String("request_id", requestID),
zap.String("ip", c.IP()),
zap.String("user_agent", c.Get("User-Agent")),
zap.String("user_id", userID),
)
5. API Response Model
Unified Response Structure
File: pkg/response/response.go
// Response is the unified API response structure
type Response struct {
Code int `json:"code"` // Application error code (0 = success)
Data interface{} `json:"data"` // Response data (object, array, or null)
Message string `json:"msg"` // Human-readable message
Timestamp string `json:"timestamp"` // ISO 8601 timestamp (optional, can be added)
}
// Success returns a successful response
func Success(c *fiber.Ctx, data interface{}) error {
return c.JSON(Response{
Code: 0,
Data: data,
Message: "success",
Timestamp: time.Now().Format(time.RFC3339),
})
}
// Error returns an error response
func Error(c *fiber.Ctx, httpStatus int, code int, message string) error {
return c.Status(httpStatus).JSON(Response{
Code: code,
Data: nil,
Message: message,
Timestamp: time.Now().Format(time.RFC3339),
})
}
// SuccessWithMessage returns a successful response with custom message
func SuccessWithMessage(c *fiber.Ctx, data interface{}, message string) error {
return c.JSON(Response{
Code: 0,
Data: data,
Message: message,
Timestamp: time.Now().Format(time.RFC3339),
})
}
Success Response Example:
{
"code": 0,
"data": {
"id": "user-123",
"name": "John Doe",
"email": "john@example.com"
},
"msg": "success",
"timestamp": "2025-11-10T15:30:45Z"
}
Error Response Example:
{
"code": 1002,
"data": null,
"msg": "Invalid or expired token",
"timestamp": "2025-11-10T15:30:45Z"
}
List Response Example:
{
"code": 0,
"data": [
{"id": "1", "name": "Item 1"},
{"id": "2", "name": "Item 2"}
],
"msg": "success",
"timestamp": "2025-11-10T15:30:45Z"
}
6. Error Model
Error Code Constants
File: pkg/errors/codes.go
// Application error codes
const (
CodeSuccess = 0 // Success
CodeInternalError = 1000 // Internal server error
CodeMissingToken = 1001 // Missing authentication token
CodeInvalidToken = 1002 // Invalid or expired token
CodeTooManyRequests = 1003 // Too many requests (rate limited)
CodeAuthServiceUnavailable = 1004 // Authentication service unavailable (Redis down)
)
// Error messages (bilingual)
var errorMessages = map[int]struct {
EN string
ZH string
}{
CodeSuccess: {"Success", "成功"},
CodeInternalError: {"Internal server error", "内部服务器错误"},
CodeMissingToken: {"Missing authentication token", "缺失认证令牌"},
CodeInvalidToken: {"Invalid or expired token", "令牌无效或已过期"},
CodeTooManyRequests: {"Too many requests", "请求过于频繁"},
CodeAuthServiceUnavailable: {"Authentication service unavailable", "认证服务不可用"},
}
// GetMessage returns error message for given code and language
func GetMessage(code int, lang string) string {
msg, ok := errorMessages[code]
if !ok {
return "Unknown error"
}
if lang == "zh" {
return msg.ZH
}
return msg.EN
}
Custom Error Types
File: pkg/errors/errors.go
import "errors"
// Standard error types for middleware
var (
ErrMissingToken = errors.New("missing authentication token")
ErrInvalidToken = errors.New("invalid or expired token")
ErrRedisUnavailable = errors.New("redis unavailable")
ErrTooManyRequests = errors.New("too many requests")
)
// AppError represents an application error with code
type AppError struct {
Code int // Application error code
Message string // Error message
Err error // Underlying error (optional)
}
func (e *AppError) Error() string {
if e.Err != nil {
return fmt.Sprintf("%s: %v", e.Message, e.Err)
}
return e.Message
}
func (e *AppError) Unwrap() error {
return e.Err
}
// New creates a new AppError
func New(code int, message string) *AppError {
return &AppError{
Code: code,
Message: message,
}
}
// Wrap wraps an existing error with code and message
func Wrap(code int, message string, err error) *AppError {
return &AppError{
Code: code,
Message: message,
Err: err,
}
}
7. Rate Limit State Model
Rate Limit Tracking
Storage: In-memory (default) or Redis (for distributed)
Managed by: Fiber limiter middleware (internal storage)
// Rate limit state is managed internally by Fiber limiter
// For memory storage: map[string]*limiterEntry
// For Redis storage: Redis keys with TTL
// Memory storage structure (internal to Fiber)
type limiterEntry struct {
count int // Current request count
expiration time.Time // Window expiration time
}
// Redis storage structure (if using Redis storage)
// Key: "ratelimit:{ip_address}"
// Value: JSON {"count": 5, "expiration": "2025-11-10T15:31:00Z"}
// TTL: Same as expiration window
Key Generation (for custom Redis storage):
// pkg/constants/redis.go
func RedisRateLimitKey(ip string) string {
return fmt.Sprintf("ratelimit:%s", ip)
}
Access Pattern:
- Middleware checks rate limit state before handler
- Increment counter on each request
- Reset counter when window expires
- Return 429 when limit exceeded
Entity Relationship Diagram
┌─────────────────┐
│ Configuration │ (YAML file)
│ - Server │
│ - Redis │
│ - Logging │
│ - Middleware │
└────────┬────────┘
│ loads into
▼
┌─────────────────┐ ┌──────────────────┐
│ Application │────▶│ Redis Client │
│ (main.go) │ │ (connection │
└────────┬────────┘ │ pool) │
│ └──────────┬───────┘
│ creates │
▼ │ validates tokens
┌─────────────────┐ │
│ Middleware │ │
│ Chain: │ │
│ - Recover │ │
│ - RequestID │ │
│ - Logger │ │
│ - KeyAuth │◀───────────────┘
│ - RateLimiter │
└────────┬────────┘
│ processes
▼
┌─────────────────┐ ┌──────────────────┐
│ Handlers │────▶│ Response │
│ (API │ │ {code, data, │
│ endpoints) │ │ msg} │
└─────────────────┘ └──────────────────┘
│
│ logs to
▼
┌─────────────────┐
│ Log Files │
│ - app.log │ (Zap + Lumberjack)
│ - access.log │
└─────────────────┘
Data Flow Diagram
Request Processing Flow
1. HTTP Request arrives
↓
2. Recover Middleware (catch panics)
↓
3. RequestID Middleware (generate UUID v4)
→ Store in c.Locals("requestid")
↓
4. Logger Middleware (start)
→ Store start_time in c.Locals("start_time")
↓
5. KeyAuth Middleware
→ Extract token from header
→ Call TokenValidator.Validate(token)
→ Validate with Redis: GET auth:token:{token}
→ If valid: Store user_id in c.Locals("user_id")
→ If invalid: Return 401 with error code
→ If Redis down: Return 503 with error code
↓
6. [RateLimiter Middleware] (if enabled)
→ Check rate limit for c.IP()
→ If exceeded: Return 429 with error code
↓
7. Handler (business logic)
→ Access c.Locals("requestid"), c.Locals("user_id")
→ Process request
→ Return response via response.Success() or response.Error()
↓
8. Logger Middleware (end)
→ Calculate duration
→ Log to access.log with all context
↓
9. HTTP Response sent
Summary
Key Entities:
- Config: Application configuration (YAML → struct)
- AuthToken: Redis key-value (token → user ID)
- RequestContext: Fiber Locals (requestid, user_id, start_time)
- LogEntry: JSON logs (app.log, access.log)
- Response: Unified API response ({code, data, msg})
- Error: Error codes and custom types
- RateLimitState: Managed by Fiber limiter (memory or Redis)
Design Principles:
- ✅ Simple, flat structures (no deep nesting)
- ✅ Direct field access (no getters/setters)
- ✅ Composition over inheritance
- ✅ Explicit error handling
- ✅ Go naming conventions (URL, ID, HTTP)
- ✅ No Java-style patterns (no I-prefix, no Impl-suffix)
Next: Generate API contracts (OpenAPI specification)