The Problem

Currently, we have to manually call c.Validate() in every handler:
func (h *AuthHandler) Login(c echo.Context) error {
    var req LoginRequest
    if err := c.Bind(&req); err != nil {
        return err
    }
    if err := c.Validate(&req); err != nil {  // ❌ Repetitive!
        return err
    }
    // ... business logic
}
Issues:
  • ❌ Repetitive code in every handler
  • ❌ Easy to forget to validate
  • ❌ Violates DRY (Don’t Repeat Yourself)

The Solution: Validation Middleware

How It Works

Automatic Model Validation

Implementation

1. Validation Middleware

// middleware/validation_middleware.go
package middleware

import "github.com/labstack/echo/v4"

func AutoValidate() echo.MiddlewareFunc {
    return func(next echo.HandlerFunc) echo.HandlerFunc {
        return func(c echo.Context) error {
            // Wrap the context with our validation wrapper
            ctx := &validationContext{Context: c}
            return next(ctx)
        }
    }
}

// validationContext wraps echo.Context
type validationContext struct {
    echo.Context
}

// Bind overrides the default Bind to auto-validate
func (vc *validationContext) Bind(i interface{}) error {
    // 1. Bind JSON to struct
    if err := vc.Context.Bind(i); err != nil {
        return err
    }

    // 2. Automatically validate
    if err := vc.Context.Validate(i); err != nil {
        return err
    }

    return nil
}

2. Register Middleware in main.go

// cmd/server/main.go
func main() {
    // ... setup ...

    e := echo.New()
    e.Validator = validator.New()

    // Global middleware
    e.Use(middleware.Logger())
    e.Use(middleware.Recover())
    e.Use(customMiddleware.AutoValidate())  // ✅ Add this

    // Routes
    api := e.Group("/api/v1")
    authHandler.RegisterRoutes(api)
}

3. Simplified Handlers

Before (Manual Validation):
func (h *AuthHandler) Login(c echo.Context) error {
    var req LoginRequest
    if err := c.Bind(&req); err != nil {
        return err
    }
    if err := c.Validate(&req); err != nil {  // ❌ Manual
        return err
    }

    tokens, user, err := h.authService.Login(ctx, ports.LoginParams{
        Email:    req.Email,
        Password: req.Password,
    })

    return c.JSON(http.StatusOK, tokens)
}
After (Automatic Validation):
func (h *AuthHandler) Login(c echo.Context) error {
    var req LoginRequest
    if err := c.Bind(&req); err != nil {
        return err  // ✅ Validation happens automatically!
    }

    tokens, user, err := h.authService.Login(ctx, ports.LoginParams{
        Email:    req.Email,
        Password: req.Password,
    })

    return c.JSON(http.StatusOK, tokens)
}

How the Middleware Works Internally

Step-by-Step Execution

// 1. Request comes in
POST /api/v1/auth/login
{
  "email": "test@example.com",
  "password": "short"  // Invalid: too short
}

// 2. AutoValidate middleware wraps the context
ctx := &validationContext{Context: originalContext}

// 3. Handler calls c.Bind(&req)
var req LoginRequest
c.Bind(&req)  // This calls validationContext.Bind()

// 4. validationContext.Bind() does TWO things:
//    a) Bind JSON to struct
originalContext.Bind(&req)  // req.Email = "test@example.com"

//    b) Validate struct tags
originalContext.Validate(&req)  // ❌ Fails: password too short

// 5. Returns validation error
{
  "message": "Password must be at least 8 characters"
}

Context Wrapping Pattern

The Decorator Pattern

// Original Echo Context
type Context interface {
    Bind(i interface{}) error
    Validate(i interface{}) error
    JSON(code int, i interface{}) error
    // ... many other methods
}

// Our Wrapper
type validationContext struct {
    echo.Context  // Embed original context
}

// Override only Bind()
func (vc *validationContext) Bind(i interface{}) error {
    // Custom logic
    if err := vc.Context.Bind(i); err != nil {
        return err
    }
    return vc.Context.Validate(i)  // Auto-validate
}

// All other methods (JSON, Param, etc.) pass through to echo.Context

Advanced: Conditional Validation

Skip Validation for Certain Routes

func AutoValidate() echo.MiddlewareFunc {
    return func(next echo.HandlerFunc) echo.HandlerFunc {
        return func(c echo.Context) error {
            // Skip validation for health check
            if c.Path() == "/health" {
                return next(c)
            }

            // Skip validation for file uploads
            if c.Request().Header.Get("Content-Type") == "multipart/form-data" {
                return next(c)
            }

            // Apply validation for all other routes
            ctx := &validationContext{Context: c}
            return next(ctx)
        }
    }
}

Comparison: Manual vs Automatic

Manual Validation (Current)

Pros:
  • ✅ Explicit (you see what’s happening)
  • ✅ Fine-grained control
Cons:
  • ❌ Repetitive code
  • ❌ Easy to forget
  • ❌ Violates DRY
Code:
func Handler(c echo.Context) error {
    var req Request
    if err := c.Bind(&req); err != nil {
        return err
    }
    if err := c.Validate(&req); err != nil {  // Repetitive
        return err
    }
    // logic
}

Automatic Validation (Middleware)

Pros:
  • ✅ DRY (Don’t Repeat Yourself)
  • ✅ Can’t forget to validate
  • ✅ Cleaner handlers
Cons:
  • ⚠️ Less explicit (validation is “hidden”)
  • ⚠️ Slightly harder to debug
Code:
func Handler(c echo.Context) error {
    var req Request
    if err := c.Bind(&req); err != nil {
        return err  // Validation happens automatically
    }
    // logic
}

When to Use Each Approach

Use Manual Validation When:

  • You need fine-grained control
  • You want explicit validation for clarity
  • You have conditional validation logic
  • You’re learning the framework

Use Automatic Validation When:

  • You have many endpoints
  • You want consistent validation across all routes
  • You prefer DRY code
  • You’re building a production API

Testing the Middleware

Test 1: Valid Request

curl -X POST http://localhost:8080/api/v1/auth/login \
  -H "Content-Type: application/json" \
  -d '{"email":"test@example.com","password":"password123"}'

# Response: 200 OK
{
  "accessToken": "...",
  "refreshToken": "..."
}

Test 2: Invalid Email

curl -X POST http://localhost:8080/api/v1/auth/login \
  -H "Content-Type: application/json" \
  -d '{"email":"not-an-email","password":"password123"}'

# Response: 400 Bad Request
{
  "message": "Email must be a valid email"
}

Test 3: Missing Field

curl -X POST http://localhost:8080/api/v1/auth/login \
  -H "Content-Type: application/json" \
  -d '{"email":"test@example.com"}'

# Response: 400 Bad Request
{
  "message": "Password is required"
}

Alternative Approach: Generic Validator Function

If you don’t want middleware, you can create a helper function:
// pkg/validator/helper.go
func BindAndValidate(c echo.Context, i interface{}) error {
    if err := c.Bind(i); err != nil {
        return err
    }
    return c.Validate(i)
}

// Usage in handler
func (h *AuthHandler) Login(c echo.Context) error {
    var req LoginRequest
    if err := validator.BindAndValidate(c, &req); err != nil {
        return err
    }
    // logic
}
Pros:
  • ✅ Explicit
  • ✅ Reusable
  • ✅ No middleware magic
Cons:
  • ⚠️ Still need to call it in every handler

Recommendation

  1. Use Automatic Validation Middleware for most routes
    • Cleaner code
    • Consistent validation
    • Less chance of forgetting
  2. Keep Manual Validation for special cases
    • File uploads
    • Webhooks
    • Complex multi-step forms
  3. Document the behavior in API docs
    • Developers should know validation happens automatically

Summary

Automatic Validation Middleware:
  • Wraps echo.Context with a custom context
  • Overrides Bind() to automatically call Validate()
  • Eliminates repetitive c.Validate() calls
  • Makes handlers cleaner and more maintainable