# 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