Request Validation
Every external request is untrusted.
Validation is therefore unavoidable —
but validation placed at the wrong layer is worse than no validation at all.
Plumego intentionally does not provide a built-in, automatic validation framework.
This is not an omission.
It is a recognition that validation strategy is an architectural decision, not a framework default.
This guide explains how to perform request validation in Plumego without violating boundaries or polluting core logic.
Two Fundamentally Different Kinds of Validation
Before writing any validation code, it is critical to distinguish between two conceptually different concerns.
1. Request Validation (Boundary Validation)
Request validation answers the question:
“Can this request be understood by the system?”
It focuses on:
- Payload format
- Required fields
- Basic type correctness
- Obviously invalid values
This is boundary-level validation.
2. Business Validation (Domain Validation)
Business validation answers a very different question:
“Is this operation allowed?”
It focuses on:
- Business rules
- State transitions
- Invariants
- Permissions and intent
This is domain and application logic.
Mixing these two forms of validation leads directly to architectural decay.
Validation Layering Principles
Plumego encourages a layered validation model, where each layer validates only what it owns.
| Validation Type | Proper Layer |
|---|---|
| JSON syntax | Handler |
| Required fields | Handler |
| Basic type / range checks | Handler |
| Authentication | Middleware |
| Authorization | Handler / Usecase |
| Business rules | Domain |
| Cross-entity rules | Usecase |
There is no single “validation layer”.
Each layer validates only what it is responsible for.
What Belongs in Handlers
Handlers sit at the HTTP boundary.
Their validation goal is simple:
“Is this request structurally valid enough to enter the system?”
Appropriate Handler-Level Validation
Handlers should validate:
- Whether the request body can be parsed
- Whether required fields are present
- Whether basic values are sane (empty, negative, malformed)
Example:
type CreateUserRequest struct {
Email string `json:"email"`
Age int `json:"age"`
}
func CreateUserHandler(ctx *plumego.Context) {
var req CreateUserRequest
if err := ctx.BindJSON(&req); err != nil {
ctx.JSON(http.StatusBadRequest, errorResponse("invalid JSON"))
return
}
if req.Email == "" {
ctx.JSON(http.StatusBadRequest, errorResponse("email is required"))
return
}
if req.Age <= 0 {
ctx.JSON(http.StatusBadRequest, errorResponse("age must be positive"))
return
}
// Passed boundary validation
}
This validation is:
- Explicit
- Local
- Easy to reason about
What Does Not Belong in Handlers
Handlers must not validate:
- Whether a user already exists
- Whether an operation is allowed in the current state
- Whether a balance is sufficient
- Whether a rule is violated
Handlers should not know why an operation is forbidden —
only how to translate the result into HTTP.
Validation in the Usecase Layer
Usecases validate process-level and authorization-level rules.
Examples include:
- Whether a user may perform an action
- Whether a workflow step is allowed
- Whether multiple domain objects can interact
Example:
func (u *CreateOrderUsecase) Execute(input Input) error {
if !u.permission.CanCreateOrder(input.UserID) {
return ErrForbidden
}
// Coordinate domain logic
return nil
}
Usecase validation is:
- Context-aware
- Workflow-driven
- Explicit in intent
Validation in the Domain Layer
The domain layer enforces invariants — rules that must always hold.
Example:
func (o *Order) Cancel() error {
if o.Status == Shipped {
return ErrCannotCancel
}
o.Status = Cancelled
return nil
}
Domain validation characteristics:
- Independent of HTTP
- Independent of identity sources
- Always correct, regardless of caller
If a domain invariant fails, the system has been used incorrectly.
Avoiding “Automatic Validation Frameworks”
In Plumego-style systems, be cautious with:
- Struct-tag-driven auto validators
- Reflection-heavy global validation
- One-line
Validate()calls that hide logic
Problems with automatic validation:
- Validation location becomes implicit
- Boundary and business validation get mixed
- Complex rules become awkward or impossible
- Failure reasons become opaque
Explicit validation may be verbose,
but verbosity is preferable to hidden behavior.
Mapping Validation Failures to HTTP Responses
Recommended conventions:
- Boundary validation failure → 400 Bad Request
- Authentication failure → 401 Unauthorized
- Authorization failure → 403 Forbidden
- Business rule violation → Domain / Usecase error → mapped by handler
Domain and usecase code must never return HTTP status codes.
Avoiding Duplicate Validation
A common mistake is defensive over-validation:
- Handler validates
- Usecase re-validates the same condition
- Domain validates again
This usually indicates unclear responsibility boundaries.
Correct approach:
- Each layer validates only what it owns
- No “just in case” validation
- No duplicated rules
Validation and Testing
Clear validation boundaries dramatically simplify testing:
- Handler tests focus on malformed input
- Usecase tests focus on workflow and permissions
- Domain tests focus on invariants
Each test suite becomes smaller and more precise.
Common Anti-Patterns
Putting All Validation in Handlers
Results in:
- Bloated handlers
- Scattered business rules
- Poor reusability
Validating HTTP Data Inside Domain Code
This is a severe boundary violation.
Domain logic must not know what a “request” is.
Using One Validator to Rule Them All
This is an attempt to avoid architectural thinking —
and usually creates more problems than it solves.
Summary
In Plumego:
- Validation is a chain, not a single step
- Each layer validates its own responsibility
- Handlers validate structure
- Usecases validate process
- Domains validate invariants
Correct validation placement is one of the highest-leverage architectural decisions you can make.
Next
If you now understand:
- Thin Handlers
- Middleware
- Request Validation
The next recommended reading is:
→ Patterns / Explicit Middleware Chains
This elevates middleware from a utility to an architectural building block.