Thin Handlers
“Keep handlers thin” is a common recommendation.
But without a concrete definition, it quickly becomes meaningless.
In Plumego, thin handlers are not a style preference —
they are a structural requirement that protects boundaries, testability, and long-term maintainability.
This document defines what “thin” actually means,
and how to enforce it in real systems.
What a Handler Is — and Is Not
A handler is the HTTP boundary.
It exists to translate between:
- HTTP-level concepts (request, response)
- Application-level concepts (use cases, inputs, outputs)
A handler is not:
- A place for business rules
- A workflow engine
- A persistence layer
- A dumping ground for logic
If handlers grow large, boundaries are already eroding.
The Three Responsibilities of a Thin Handler
A thin handler does exactly three things:
-
Extract input
Read parameters, headers, and request bodies. -
Invoke application logic
Call a usecase or application service. -
Translate output
Convert results and errors into HTTP responses.
If a handler does anything else, it is no longer thin.
A Minimal Thin Handler Example
func CreateOrderHandler(ctx *plumego.Context) {
var req CreateOrderRequest
if err := ctx.BindJSON(&req); err != nil {
ctx.JSON(http.StatusBadRequest, errorResponse(err))
return
}
id, err := createOrderUsecase.Execute(req.ToInput())
if err != nil {
handleError(ctx, err)
return
}
ctx.JSON(http.StatusCreated, map[string]string{
"id": id,
})
}
Notice what is not present:
- No business rules
- No persistence logic
- No conditional workflows
- No framework leakage inward
What Makes a Handler “Fat”
Handlers become fat when they start answering questions they should not.
Common fat-handler smells
- Long functions (50+ lines)
- Nested
if/elsetrees - Business terminology mixed with HTTP details
- Database calls
- Conditional logic based on domain rules
- Duplicate logic across handlers
These are structural smells, not stylistic ones.
Where Logic Should Go Instead
| Logic Type | Proper Location |
|---|---|
| HTTP parsing | Handler |
| Authentication | Middleware |
| Authorization rules | Handler / Usecase |
| Workflow orchestration | Usecase |
| Business rules | Domain |
| Persistence | Infrastructure |
| Validation (business) | Domain / Usecase |
Handlers should remain boring.
Validation: Boundary vs Business
Not all validation is equal.
Boundary validation (handler-level)
- Required fields
- Basic type checks
- Malformed payloads
Business validation (domain-level)
- Invariants
- State transitions
- Rule enforcement
Mixing the two leads to duplicated logic and inconsistencies.
Error Handling Without Fat Handlers
Handlers should not implement complex error trees.
Instead:
- Let usecases return domain/application errors
- Centralize HTTP error translation
- Keep handler error paths shallow
Example:
func handleError(ctx *plumego.Context, err error) {
switch {
case errors.Is(err, ErrForbidden):
ctx.JSON(http.StatusForbidden, errorResponse(err))
case errors.Is(err, ErrNotFound):
ctx.JSON(http.StatusNotFound, errorResponse(err))
default:
ctx.JSON(http.StatusInternalServerError, errorResponse("internal error"))
}
}
This keeps handlers readable and consistent.
Thin Handlers and Testing
Thin handlers are easy to test because:
- Inputs are explicit
- Outputs are deterministic
- Business logic is mocked or stubbed
- No hidden dependencies exist
Most bugs surface in usecases and domain logic —
exactly where they should.
Thin Handlers Enable Transport Flexibility
When handlers are thin:
- You can add new transports (RPC, CLI, workers)
- Reuse application logic
- Change HTTP frameworks if needed
Fat handlers lock your system to a single delivery mechanism.
Enforcing Thin Handlers in Practice
Thin handlers are a discipline, not an accident.
Effective enforcement techniques:
- Code review rules (handler size, imports)
- Package boundaries (
internal/http) - Naming conventions (
*Handlervs*Service) - Regular refactoring
Treat handler bloat as architectural debt.
When a Handler Is Allowed to Grow
Occasionally, handlers may grow temporarily during:
- Early prototyping
- Exploratory development
But growth must be paid back quickly.
Permanent fat handlers are a sign of architectural neglect.
Summary
In Plumego:
- Handlers are boundaries
- Boundaries translate, they do not decide
- Thin handlers preserve architecture
- Fat handlers erode it silently
Keeping handlers thin is one of the highest-leverage practices in the system.
Next
With thin handlers established, the next useful pattern is:
→ Explicit Middleware Chains
This explains how to compose middleware intentionally without creating hidden behavior.