Overview
Handlers are the outermost API boundary. They receive HTTP requests from Gin, parse and validate them, call services, and send back JSON responses. This separation keeps business logic testable and transport-agnostic.
Key principle: Handlers should be thin. If a handler exceeds ~50 lines, logic belongs in a service.
Request Processing Flow
HTTP Request
|
v
Gin Router (matches path)
|
v
Middleware (auth, CORS, logging)
|
v
Handler.Validate() (check JWT, unmarshal JSON)
|
v
Handler.Call Service (business logic)
|
v
Service returns (data, error)
|
v
Handler.Marshal Response (JSON)
|
v
Gin sends HTTP 200/4xx/5xx
Handler Types
| Handler | Endpoints | Purpose |
|---|---|---|
| ChatHandler | POST /chat, GET /chat/:id | Conversational AI. Streams responses, manages context window. |
| SearchHandler | POST /search, GET /search/stats | Full-text + semantic search. Returns ranked results with snippets. |
| KBHandler | POST /kb, DELETE /kb/:id | Knowledge base CRUD. Manages documents, indexes. |
| DocumentHandler | PUT /doc/:id, GET /doc/:id/chunks | Document operations. Upload, delete, reprocess. |
| FileHandler | POST /file/upload, GET /file/:id | File I/O wrapper. Delegates to storage backend. |
| LLMHandler | POST /llm/chat, POST /llm/embed | LLM API proxy. Routes to provider (OpenAI, local). |
| ChunkHandler | GET /chunk/:id, PATCH /chunk/:id | Text chunk operations. Inspect, edit, rerank. |
Handler Anatomy
Input Validation
Every handler validates HTTP inputs before calling services:
type ChatRequest struct {
KnowledgeBaseID string `json:"kb_id" binding:"required"`
Question string `json:"question" binding:"required,max=2000"`
MaxTokens int `json:"max_tokens" binding:"gte=10,lte=4000"`
}
func (h *ChatHandler) Chat(c *gin.Context) {
var req ChatRequest
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(400, gin.H{"error": err.Error()})
return
}
// Input is now type-safe and validated
}
Authorization
Middleware extracts JWT and injects user/tenant context. Handlers check scopes:
func (h *ChatHandler) Chat(c *gin.Context) {
userID := c.GetString("user_id") // from JWT
tenantID := c.GetString("tenant_id")
if userID == "" {
c.JSON(401, gin.H{"error": "unauthorized"})
return
}
// Ensure user owns the KB
kb, err := h.service.GetKB(c.Context(), tenantID, kbID)
if err != nil || kb.OwnerID != userID {
c.JSON(403, gin.H{"error": "forbidden"})
return
}
}
Service Invocation
Delegate to service; let it handle retries, caching, transactions:
result, err := h.chatService.Chat(c.Context(), &ChatRequest{
KnowledgeBaseID: req.KnowledgeBaseID,
Question: req.Question,
UserID: userID,
TenantID: tenantID,
})
if err != nil {
h.logger.Error("chat error", "user_id", userID, "error", err)
c.JSON(500, gin.H{"error": "internal error"})
return
}
Response Formatting
Return consistent, typed responses:
type ChatResponse struct {
ID string `json:"id"`
Answer string `json:"answer"`
Tokens int `json:"tokens"`
Sources []Source `json:"sources"`
Latency float64 `json:"latency_ms"`
}
c.JSON(200, gin.H{
"data": result,
"meta": gin.H{
"request_id": c.GetString("request_id"),
"timestamp": time.Now().Unix(),
},
})
Error Handling
| Status | When | Example |
|---|---|---|
| 400 | Invalid input (missing field, wrong type) | missing kb_id parameter |
| 401 | Missing or invalid JWT | expired token |
| 403 | User lacks permission | accessing other tenant's KB |
| 404 | Resource not found | KB not in database |
| 429 | Rate limit exceeded | user quota hit |
| 500 | Unexpected service error | database connection lost |
Submodules
Request Lifecycle Deep Dive
Trace a search query from client to response. End-to-end timing breakdown.
Middleware
CORS, JWT extraction, request logging, panic recovery.
Chat Handler Details
Streaming responses, context management, token accounting.
Search Handler Details
Query parsing, ranking algorithms, result pagination.
Authorization Patterns
JWT validation, tenant isolation, role-based access control.
Response Formats
Consistent JSON schema, error codes, timing metadata.
Key Concepts
Gin Context Injection
Handlers receive *gin.Context, which carries request state (user, tenant, request ID, headers). This allows middleware to inject data that handlers read.
Unmarshaling & Binding
c.ShouldBindJSON() combines unmarshaling and validation in one call. Errors are caught early.
Layering Discipline
Handlers must not access the database directly. They must call services. This maintains separation of concerns and makes unit tests faster.
Common Pitfalls
- Too much logic in handlers: Handlers should be <5 KB. Move complex logic to services.
- Forgetting to validate input: Always call
ShouldBindJSON()or equivalent. Don't trust client input. - Inconsistent error responses: Return a consistent error schema. Document it in OpenAPI.
- Not checking authorization: Every handler that accesses user data must verify ownership via tenant or user ID.
- Direct database access: Handlers should never call DAO. That's the service's job.
Common Patterns
Request ID Tracking
Every request gets a unique ID in middleware. This ID flows through logs, allowing you to trace a single request through all systems.
func LoggingMiddleware() gin.HandlerFunc {
return func(c *gin.Context) {
requestID := uuid.New().String()
c.Set("request_id", requestID)
c.Header("X-Request-ID", requestID)
c.Next()
latency := time.Since(start)
logger.Info("request", "request_id", requestID, "latency_ms", latency.Milliseconds())
}
}
Timeout & Cancellation
All handlers pass context.Context down to services and DAOs. Set a timeout in middleware so slow requests don't accumulate.
func TimeoutMiddleware(timeout time.Duration) gin.HandlerFunc {
return func(c *gin.Context) {
ctx, cancel := context.WithTimeout(c.Request.Context(), timeout)
defer cancel()
c.Request = c.Request.WithContext(ctx)
c.Next()
}
}
Graceful Shutdown
When the server shuts down, drain in-flight requests before closing connections.
server := &http.Server{Addr: ":8080", Handler: router}
go func() {
<-shutdown // signal channel
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
server.Shutdown(ctx) // wait up to 5s for requests to finish
}()
server.ListenAndServe()
Testing Handlers
Use Gin's test utilities to mock HTTP requests:
func TestSearchHandler(t *testing.T) {
router := gin.Default()
mockService := &MockSearchService{}
handler := &SearchHandler{searchService: mockService}
router.POST("/search", handler.Search)
body := `{"query": "test", "kb_id": "kb1", "top_k": 10}`
req, _ := http.NewRequest("POST", "/search", strings.NewReader(body))
w := httptest.NewRecorder()
router.ServeHTTP(w, req)
assert.Equal(t, 200, w.Code)
}
Response Formatting Best Practices
Standard Response Envelope
Wrap all responses in a consistent envelope for client-side consistency:
type APIResponse struct {
Code int `json:"code"` // 0 = success, non-zero = error
Message string `json:"message"` // human-readable message
Data interface{} `json:"data"` // actual response data
Meta struct {
RequestID string `json:"request_id"`
Timestamp int64 `json:"timestamp"`
} `json:"meta"`
}
// Usage
c.JSON(200, APIResponse{
Code: 0,
Message: "OK",
Data: searchResult,
Meta: {
RequestID: c.GetString("request_id"),
Timestamp: time.Now().Unix(),
},
})
Pagination Support
For list endpoints, support offset/limit pagination:
type ListRequest struct {
Offset int `json:"offset" binding:"gte=0"`
Limit int `json:"limit" binding:"gte=1,lte=100"`
}
// Query
var items []Item
d.db.Offset(offset).Limit(limit).Find(&items)
// Response includes total count
type ListResponse struct {
Items []Item `json:"items"`
Total int `json:"total"`
}
Error Response Standards
Provide detailed error information for debugging:
type ErrorResponse struct {
Code int `json:"code"`
Message string `json:"message"`
Details string `json:"details,omitempty"` // internal error details
Timestamp int64 `json:"timestamp"`
Path string `json:"path"`
}
// Example: Missing required field
c.JSON(400, ErrorResponse{
Code: 400,
Message: "validation error",
Details: "field 'kb_id' is required",
Path: c.Request.URL.Path,
Timestamp: time.Now().Unix(),
})
Further Reading
- Request Lifecycle Deep Dive — Follow a chat query end-to-end
- Service Layer — Where handlers delegate
- Tokenizer" href="../index.html">Internal Module Overview