veza/docs/archive/root-md/REQUEST_RESPONSE_VALIDATION_GUIDE.md
senke 43af35fd93 chore(audit 2.2, 2.3): nettoyer .md et .json à la racine
- Archiver 131 .md dans docs/archive/root-md/
- Archiver 22 .json dans docs/archive/root-json/
- Conserver 7 .md utiles (README, CONTRIBUTING, CHANGELOG, etc.)
- Conserver package.json, package-lock.json, turbo.json
- Ajouter README d'index dans chaque archive
2026-02-15 14:35:08 +01:00

549 lines
14 KiB
Markdown

# Request/Response Validation Guide
## INT-012: Add request/response validation
**Date**: 2025-12-25
**Status**: Completed
## Overview
This guide provides comprehensive documentation for request and response validation in the Veza Backend API. It covers validation strategies, best practices, and implementation guidelines.
## Current Implementation
The Veza API uses a centralized validation system built on:
- **go-playground/validator**: Core validation library
- **Gin binding**: Request binding and basic validation
- **Custom validators**: Domain-specific validation rules
- **Centralized helpers**: `BindAndValidateJSON` for consistent validation
## Validation Architecture
### Components
1. **Validator** (`internal/validators/validator.go`)
- Centralized validator instance
- Custom validation rules
- Error message formatting
2. **Common Helpers** (`internal/common/validation.go`)
- `BindAndValidateJSON`: Main validation helper
- Error handling
- Body size limits
3. **Middleware** (`internal/middleware/validation.go`)
- Query parameter validation
- Request-level validation
4. **DTOs** (Data Transfer Objects)
- Request structures with validation tags
- Response structures (when needed)
## Request Validation
### Using BindAndValidateJSON
The recommended way to validate requests:
```go
func (h *Handler) CreateResource(c *gin.Context) {
var req CreateResourceRequest
// Validate request
if appErr := h.commonHandler.BindAndValidateJSON(c, &req); appErr != nil {
RespondWithAppError(c, appErr)
return
}
// Process validated request
// ...
}
```
### Validation Tags
#### Standard Tags (go-playground/validator)
```go
type CreateUserRequest struct {
// Required fields
Email string `json:"email" binding:"required,email" validate:"required,email"`
Username string `json:"username" binding:"required,min=3,max=30" validate:"required,min=3,max=30,username"`
// Optional fields
Bio string `json:"bio" binding:"omitempty,max=500" validate:"omitempty,max=500"`
// Numeric validation
Age int `json:"age" binding:"omitempty,min=18,max=120" validate:"omitempty,min=18,max=120"`
// UUID validation
UserID string `json:"user_id" binding:"required,uuid" validate:"required,uuid"`
// Enum validation
Role string `json:"role" binding:"required,oneof=user admin moderator" validate:"required,oneof=user admin moderator"`
// URL validation
Website string `json:"website" binding:"omitempty,url" validate:"omitempty,url"`
}
```
#### Custom Tags
The API includes custom validation tags:
- **`username`**: Alphanumeric + underscore, 3-30 characters
- **`uuid_string`**: UUID format validation
- **`slug`**: URL-friendly slug format
- **`phone`**: International phone number format
- **`date_iso`**: ISO 8601 date format (YYYY-MM-DD)
- **`not_empty`**: Non-empty string after trim
```go
type UpdateProfileRequest struct {
Username string `json:"username" validate:"required,username"`
Slug string `json:"slug" validate:"omitempty,slug"`
Phone string `json:"phone" validate:"omitempty,phone"`
BirthDate string `json:"birth_date" validate:"omitempty,date_iso"`
}
```
### Common Validation Patterns
#### Required Fields
```go
Field string `json:"field" binding:"required" validate:"required"`
```
#### String Length
```go
Title string `json:"title" binding:"required,min=1,max=200" validate:"required,min=1,max=200"`
```
#### Numeric Ranges
```go
Price float64 `json:"price" binding:"required,min=0,gt=0" validate:"required,min=0,gt=0"`
Page int `json:"page" binding:"omitempty,min=1" validate:"omitempty,min=1"`
```
#### Enums
```go
Status string `json:"status" binding:"required,oneof=active inactive pending" validate:"required,oneof=active inactive pending"`
```
#### UUIDs
```go
TrackID string `json:"track_id" binding:"required,uuid" validate:"required,uuid"`
```
#### Optional Fields
```go
Description string `json:"description" binding:"omitempty,max=1000" validate:"omitempty,max=1000"`
```
### Query Parameter Validation
Use the query parameter validation middleware:
```go
queryValidation := middleware.NewQueryParamValidation(logger)
router.GET("/tracks",
queryValidation.ValidateQueryParams(map[string]string{
"page": "numeric,min=1",
"limit": "numeric,min=1,max=100",
"sort": "oneof=asc,desc",
}),
handler.ListTracks,
)
```
### Path Parameter Validation
Validate path parameters manually:
```go
func (h *Handler) GetTrack(c *gin.Context) {
trackID := c.Param("id")
// Validate UUID format
if _, err := uuid.Parse(trackID); err != nil {
RespondWithAppError(c, apperrors.NewValidationError("Invalid track ID format"))
return
}
// Continue processing
}
```
## Response Validation
### When to Validate Responses
Response validation is typically not needed in production, but can be useful for:
- Development/testing
- API contract testing
- Ensuring consistency
### Response Structure Validation
Ensure responses follow the standard format:
```go
// Standard success response
RespondSuccess(c, http.StatusOK, gin.H{
"data": result,
})
// Standard error response
RespondWithAppError(c, apperrors.NewValidationError("Validation failed"))
```
### Response Schema Validation (Optional)
For strict response validation, you can validate response structures:
```go
type TrackResponse struct {
ID string `json:"id" validate:"required,uuid"`
Title string `json:"title" validate:"required,min=1,max=200"`
Duration int `json:"duration" validate:"required,min=0"`
}
func validateResponse(data interface{}) error {
validator := validators.NewValidator()
errors := validator.Validate(data)
if len(errors) > 0 {
return fmt.Errorf("response validation failed: %v", errors)
}
return nil
}
```
## Validation Error Format
### Standard Error Response
All validation errors follow the standard API response format:
```json
{
"success": false,
"error": {
"code": 1000,
"message": "Validation failed",
"details": [
{
"field": "email",
"message": "The field 'email' must be a valid email address",
"value": "invalid-email"
},
{
"field": "username",
"message": "The field 'username' must be at least 3 characters long",
"value": "ab"
}
],
"timestamp": "2025-12-25T10:30:00Z",
"request_id": "550e8400-e29b-41d4-a716-446655440000"
}
}
```
### Error Codes
- **1000** (`ErrCodeValidation`): General validation error
- **1001**: Required field missing
- **1002**: Invalid format
- **1003**: Value out of range
## Best Practices
### 1. Always Validate Input
```go
// ✅ Good
var req CreateRequest
if appErr := h.commonHandler.BindAndValidateJSON(c, &req); appErr != nil {
RespondWithAppError(c, appErr)
return
}
// ❌ Bad
var req CreateRequest
c.ShouldBindJSON(&req) // No validation, no error handling
```
### 2. Use Both Binding and Validation Tags
```go
// ✅ Good
Email string `json:"email" binding:"required,email" validate:"required,email"`
// ⚠️ Acceptable (but less robust)
Email string `json:"email" binding:"required,email"`
```
### 3. Validate Path Parameters
```go
// ✅ Good
trackID := c.Param("id")
if _, err := uuid.Parse(trackID); err != nil {
RespondWithAppError(c, apperrors.NewValidationError("Invalid track ID"))
return
}
// ❌ Bad
trackID := c.Param("id") // No validation
```
### 4. Validate Query Parameters
```go
// ✅ Good
page, err := strconv.Atoi(c.DefaultQuery("page", "1"))
if err != nil || page < 1 {
RespondWithAppError(c, apperrors.NewValidationError("Invalid page number"))
return
}
// ✅ Better (using middleware)
queryValidation.ValidateQueryParams(map[string]string{
"page": "numeric,min=1",
})
```
### 5. Provide Clear Error Messages
```go
// ✅ Good - Custom validator with clear messages
Username string `json:"username" validate:"required,username"`
// ❌ Bad - Generic error
Username string `json:"username" validate:"required"`
```
### 6. Set Appropriate Limits
```go
// ✅ Good - Reasonable limits
Title string `json:"title" validate:"required,min=1,max=200"`
// ❌ Bad - No limits or unrealistic limits
Title string `json:"title" validate:"required"`
```
### 7. Validate Nested Structures
```go
type CreatePlaylistRequest struct {
Title string `json:"title" validate:"required,min=1,max=200"`
Tracks []TrackReference `json:"tracks" validate:"omitempty,dive"`
}
type TrackReference struct {
ID string `json:"id" validate:"required,uuid"`
Position int `json:"position" validate:"omitempty,min=0"`
}
```
## Common Validation Scenarios
### User Registration
```go
type RegisterRequest struct {
Email string `json:"email" binding:"required,email" validate:"required,email"`
Username string `json:"username" binding:"required,min=3,max=30" validate:"required,min=3,max=30,username"`
Password string `json:"password" binding:"required,min=12" validate:"required,min=12"`
PasswordConfirm string `json:"password_confirm" binding:"required,eqfield=Password" validate:"required,eqfield=Password"`
}
```
### Pagination
```go
type PaginationParams struct {
Page int `json:"page" binding:"omitempty,min=1" validate:"omitempty,min=1"`
Limit int `json:"limit" binding:"omitempty,min=1,max=100" validate:"omitempty,min=1,max=100"`
}
```
### File Upload
```go
type UploadRequest struct {
Filename string `json:"filename" binding:"required,min=1,max=255" validate:"required,min=1,max=255"`
FileSize int64 `json:"file_size" binding:"required,min=1,max=10737418240" validate:"required,min=1,max=10737418240"` // 10GB max
MimeType string `json:"mime_type" binding:"required" validate:"required"`
}
```
### Date Ranges
```go
type DateRangeRequest struct {
StartDate string `json:"start_date" binding:"omitempty,date_iso" validate:"omitempty,date_iso"`
EndDate string `json:"end_date" binding:"omitempty,date_iso" validate:"omitempty,date_iso"`
}
```
## Testing Validation
### Unit Tests
```go
func TestCreateUserRequest_Validation(t *testing.T) {
validator := validators.NewValidator()
tests := []struct {
name string
request CreateUserRequest
wantErr bool
}{
{
name: "valid request",
request: CreateUserRequest{
Email: "user@example.com",
Username: "testuser",
},
wantErr: false,
},
{
name: "invalid email",
request: CreateUserRequest{
Email: "invalid-email",
Username: "testuser",
},
wantErr: true,
},
{
name: "username too short",
request: CreateUserRequest{
Email: "user@example.com",
Username: "ab",
},
wantErr: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
errors := validator.Validate(tt.request)
if (len(errors) > 0) != tt.wantErr {
t.Errorf("Validation error mismatch: got %v, want %v", errors, tt.wantErr)
}
})
}
}
```
### Integration Tests
```go
func TestCreateUser_Validation(t *testing.T) {
router := setupTestRouter()
tests := []struct {
name string
body string
expectedStatus int
}{
{
name: "missing email",
body: `{"username": "testuser"}`,
expectedStatus: http.StatusBadRequest,
},
{
name: "invalid email format",
body: `{"email": "invalid", "username": "testuser"}`,
expectedStatus: http.StatusBadRequest,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
w := httptest.NewRecorder()
req := httptest.NewRequest("POST", "/api/v1/users", strings.NewReader(tt.body))
router.ServeHTTP(w, req)
assert.Equal(t, tt.expectedStatus, w.Code)
})
}
}
```
## Validation Checklist
When creating a new endpoint:
- [ ] Define request DTO with validation tags
- [ ] Use `BindAndValidateJSON` for request validation
- [ ] Validate path parameters (UUIDs, IDs)
- [ ] Validate query parameters (pagination, filters)
- [ ] Test validation with invalid inputs
- [ ] Test validation with edge cases
- [ ] Ensure error messages are clear
- [ ] Document validation rules in Swagger annotations
## Common Issues and Solutions
### Issue: Validation not working
**Solution**: Ensure both `binding` and `validate` tags are present:
```go
// ✅ Correct
Email string `json:"email" binding:"required,email" validate:"required,email"`
```
### Issue: Custom validator not recognized
**Solution**: Ensure custom validators are registered in `registerCustomValidations`:
```go
v.RegisterValidation("custom_tag", func(fl validator.FieldLevel) bool {
// Validation logic
return true
})
```
### Issue: Nested struct validation
**Solution**: Use `dive` tag for nested structures:
```go
Tracks []TrackReference `json:"tracks" validate:"omitempty,dive"`
```
### Issue: Conditional validation
**Solution**: Use custom validator or validate in handler:
```go
// In handler
if req.Type == "premium" && req.Price < 10 {
RespondWithAppError(c, apperrors.NewValidationError("Premium products must cost at least $10"))
return
}
```
## References
- [go-playground/validator Documentation](https://pkg.go.dev/github.com/go-playground/validator/v10)
- [Gin Binding Documentation](https://gin-gonic.com/docs/examples/binding-and-validation/)
- `internal/validators/validator.go` - Custom validators
- `internal/common/validation.go` - Validation helpers
- `ERROR_RESPONSE_STANDARD.md` - Error response format
---
**Last Updated**: 2025-12-25
**Maintained By**: Veza Backend Team