Skip to main content

Echo Request Validation

Introduction

When developing web applications, validating user input is a critical security practice. Invalid data can cause application errors, security vulnerabilities, or even data corruption. In this guide, we'll explore how to effectively validate requests in the Echo framework, a high-performance, extensible Go web framework.

Request validation ensures that the data you receive from clients matches your expected format before your application logic processes it. This creates a more robust, secure application and provides helpful feedback to your users when they submit invalid data.

Why Validate Requests?

Before diving into implementation, let's understand why request validation is crucial:

  1. Security: Prevents injection attacks and other security vulnerabilities
  2. Data Integrity: Ensures your application only processes valid data
  3. User Experience: Provides clear feedback when users submit incorrect data
  4. Server Stability: Reduces unexpected errors from malformed inputs
  5. Code Simplicity: Separates validation logic from business logic

Basic Validation in Echo

Let's start with a simple example of validating a user registration form:

go
package main

import (
"net/http"

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

// User represents the user registration data
type User struct {
Name string `json:"name" form:"name"`
Email string `json:"email" form:"email"`
Age int `json:"age" form:"age"`
Password string `json:"password" form:"password"`
}

func main() {
e := echo.New()

e.POST("/register", registerUser)

e.Start(":8080")
}

func registerUser(c echo.Context) error {
u := new(User)
if err := c.Bind(u); err != nil {
return echo.NewHTTPError(http.StatusBadRequest, "Invalid request format")
}

// Manual validation
if u.Name == "" {
return echo.NewHTTPError(http.StatusBadRequest, "Name is required")
}

if u.Email == "" {
return echo.NewHTTPError(http.StatusBadRequest, "Email is required")
}

if u.Age < 18 {
return echo.NewHTTPError(http.StatusBadRequest, "Must be 18 years or older")
}

if len(u.Password) < 8 {
return echo.NewHTTPError(http.StatusBadRequest, "Password must be at least 8 characters long")
}

// Process valid registration...

return c.JSON(http.StatusCreated, map[string]string{
"message": "Registration successful",
})
}

This approach works, but it becomes cumbersome as your validation rules grow more complex. Let's see how to improve this.

Using the validator Package

Echo integrates seamlessly with the popular go-playground/validator package. This library provides powerful validation using struct tags.

First, install the validator:

bash
go get github.com/go-playground/validator/v10

Now, let's implement a custom validator for Echo:

go
package main

import (
"net/http"

"github.com/go-playground/validator/v10"
"github.com/labstack/echo/v4"
)

// User with validation tags
type User struct {
Name string `json:"name" form:"name" validate:"required"`
Email string `json:"email" form:"email" validate:"required,email"`
Age int `json:"age" form:"age" validate:"required,gte=18"`
Password string `json:"password" form:"password" validate:"required,min=8"`
}

// CustomValidator holds the validator instance
type CustomValidator struct {
validator *validator.Validate
}

// Validate validates structs based on tags
func (cv *CustomValidator) Validate(i interface{}) error {
if err := cv.validator.Struct(i); err != nil {
// Return a user-friendly error message
return echo.NewHTTPError(http.StatusBadRequest, err.Error())
}
return nil
}

func main() {
e := echo.New()

// Register the validator
e.Validator = &CustomValidator{validator: validator.New()}

e.POST("/register", registerUser)

e.Start(":8080")
}

func registerUser(c echo.Context) error {
u := new(User)

// Bind the request data to the User struct
if err := c.Bind(u); err != nil {
return echo.NewHTTPError(http.StatusBadRequest, "Invalid request format")
}

// Validate the struct
if err := c.Validate(u); err != nil {
return err
}

// Process valid registration...

return c.JSON(http.StatusCreated, map[string]string{
"message": "Registration successful",
})
}

Input and Output Examples

Input (Valid Request):

json
{
"name": "John Doe",
"email": "[email protected]",
"age": 25,
"password": "securepass123"
}

Output:

json
{
"message": "Registration successful"
}

Input (Invalid Request):

json
{
"name": "John Doe",
"email": "invalid-email",
"age": 15,
"password": "short"
}

Output:

json
{
"message": "Key: 'User.Email' Error:Field validation for 'Email' failed on the 'email' tag; Key: 'User.Age' Error:Field validation for 'Age' failed on the 'gte' tag; Key: 'User.Password' Error:Field validation for 'Password' failed on the 'min' tag"
}

Common Validation Tags

Here are some commonly used validator tags:

TagDescriptionExample
requiredField must be presentvalidate:"required"
emailField must be valid emailvalidate:"email"
minMinimum length for stringsvalidate:"min=8"
maxMaximum length for stringsvalidate:"max=255"
gteGreater than or equal (numbers)validate:"gte=18"
lteLess than or equal (numbers)validate:"lte=100"
oneofValue must be one of the optionsvalidate:"oneof=admin user guest"
numericField must contain only numbersvalidate:"numeric"
alphanumField must contain only alphanumeric charactersvalidate:"alphanum"

Improving the Error Messages

The default error messages from validator are not very user-friendly. Let's improve them:

go
// CustomValidator holds the validator instance
type CustomValidator struct {
validator *validator.Validate
}

// Validate validates structs based on tags
func (cv *CustomValidator) Validate(i interface{}) error {
if err := cv.validator.Struct(i); err != nil {
// Convert validator errors to user-friendly errors
validationErrors := err.(validator.ValidationErrors)
errorMessages := make(map[string]string)

for _, e := range validationErrors {
switch e.Tag() {
case "required":
errorMessages[e.Field()] = "This field is required"
case "email":
errorMessages[e.Field()] = "Please enter a valid email address"
case "min":
errorMessages[e.Field()] = "This field must be at least " + e.Param() + " characters long"
case "gte":
errorMessages[e.Field()] = "This value must be at least " + e.Param()
default:
errorMessages[e.Field()] = "Invalid value"
}
}

return echo.NewHTTPError(http.StatusBadRequest, errorMessages)
}
return nil
}

Now, the errors will be more user-friendly:

json
{
"Email": "Please enter a valid email address",
"Age": "This value must be at least 18",
"Password": "This field must be at least 8 characters long"
}

Custom Validation Rules

Sometimes, you need custom validation logic that isn't covered by the standard validators. Here's how to create custom validation functions:

go
func main() {
e := echo.New()

v := validator.New()

// Register a custom validation function
v.RegisterValidation("strong_password", validateStrongPassword)

// Register the validator
e.Validator = &CustomValidator{validator: v}

e.POST("/register", registerUser)

e.Start(":8080")
}

// Custom validation function for strong passwords
func validateStrongPassword(fl validator.FieldLevel) bool {
password := fl.Field().String()

// Check for at least one uppercase letter
hasUpper := false
// Check for at least one digit
hasDigit := false
// Check for at least one special character
hasSpecial := false

for _, char := range password {
switch {
case unicode.IsUpper(char):
hasUpper = true
case unicode.IsDigit(char):
hasDigit = true
case unicode.IsPunct(char) || unicode.IsSymbol(char):
hasSpecial = true
}
}

return hasUpper && hasDigit && hasSpecial
}

// User with custom validation tag
type User struct {
// ... other fields
Password string `json:"password" form:"password" validate:"required,min=8,strong_password"`
}

Real-World Example: API Endpoint for Product Creation

Let's see a real-world example of validating a product creation API:

go
package main

import (
"net/http"
"time"

"github.com/go-playground/validator/v10"
"github.com/labstack/echo/v4"
)

// Product represents a product in our store
type Product struct {
Name string `json:"name" validate:"required,min=3,max=100"`
Description string `json:"description" validate:"required,min=10,max=1000"`
Price float64 `json:"price" validate:"required,gt=0"`
Categories []string `json:"categories" validate:"required,min=1,dive,required"`
SKU string `json:"sku" validate:"required,alphanum,len=10"`
InStock bool `json:"in_stock"`
CreatedAt time.Time `json:"created_at"`
}

func main() {
e := echo.New()

// Register the validator
e.Validator = &CustomValidator{validator: validator.New()}

e.POST("/products", createProduct)

e.Logger.Fatal(e.Start(":8080"))
}

func createProduct(c echo.Context) error {
p := new(Product)

// Bind the request data
if err := c.Bind(p); err != nil {
return echo.NewHTTPError(http.StatusBadRequest, "Invalid request format")
}

// Set creation time
p.CreatedAt = time.Now()

// Validate the struct
if err := c.Validate(p); err != nil {
return err
}

// Save product to database (simplified for demo)
// db.SaveProduct(p)

return c.JSON(http.StatusCreated, map[string]interface{}{
"message": "Product created successfully",
"product": p,
})
}

Input Example:

json
{
"name": "Wireless Headphones",
"description": "High-quality wireless headphones with noise cancellation",
"price": 99.99,
"categories": ["electronics", "audio"],
"sku": "ELEC123456",
"in_stock": true
}

Output Example (Success):

json
{
"message": "Product created successfully",
"product": {
"name": "Wireless Headphones",
"description": "High-quality wireless headphones with noise cancellation",
"price": 99.99,
"categories": ["electronics", "audio"],
"sku": "ELEC123456",
"in_stock": true,
"created_at": "2023-10-20T14:22:36Z"
}
}

Validating URL and Query Parameters

So far, we've focused on validating request bodies. Let's look at validating URL parameters and query strings:

go
// User search parameters
type SearchParams struct {
MinAge int `query:"min_age" validate:"omitempty,gte=0"`
MaxAge int `query:"max_age" validate:"omitempty,gte=0,gtefield=MinAge"`
City string `query:"city" validate:"omitempty,alpha"`
Limit int `query:"limit" validate:"omitempty,gte=1,lte=100"`
}

func searchUsers(c echo.Context) error {
params := new(SearchParams)

// Bind query parameters
if err := c.Bind(params); err != nil {
return echo.NewHTTPError(http.StatusBadRequest, "Invalid query parameters")
}

// Validate parameters
if err := c.Validate(params); err != nil {
return err
}

// Default values if not provided
if params.Limit == 0 {
params.Limit = 20 // default limit
}

// Perform search based on parameters...
// users := repository.SearchUsers(params)

return c.JSON(http.StatusOK, map[string]interface{}{
"params": params,
// "users": users,
})
}

This handler validates a request like /api/users?min_age=18&max_age=30&city=NewYork&limit=50.

Validation in Middleware

Sometimes you want to apply validation before the request reaches your handler. Using middleware:

go
func ValidationMiddleware(next echo.HandlerFunc) echo.HandlerFunc {
return func(c echo.Context) error {
// Get the content type
contentType := c.Request().Header.Get("Content-Type")

// Validate based on route and content type
if c.Path() == "/api/users" && c.Request().Method == "POST" {
if !strings.Contains(contentType, "application/json") {
return echo.NewHTTPError(http.StatusUnsupportedMediaType,
"Content type must be application/json")
}
}

// Continue to the next handler
return next(c)
}
}

func main() {
e := echo.New()

// Apply middleware globally
e.Use(ValidationMiddleware)

// Routes
e.POST("/api/users", createUser)

e.Start(":8080")
}

Summary

Proper request validation is an essential part of building secure, robust web applications with Echo. In this guide, we've covered:

  1. Why request validation is important
  2. Basic manual validation in Echo
  3. Using the validator package for declarative validation
  4. Customizing error messages for better user experience
  5. Creating custom validation rules
  6. Validating different types of requests (body, URL parameters, query strings)
  7. Implementing validation in middleware

By implementing these validation techniques, you can ensure your API only processes valid data, improving security, reliability, and user experience.

Additional Resources and Exercises

Resources

Exercises

  1. Basic Validation: Create a simple contact form handler that validates name (required), email (valid email format), and message (at least 10 characters).

  2. Advanced Validation: Build an API endpoint for creating a blog post with validation for title, content, tags (at least one tag), and publication date (must be present or future date).

  3. Custom Validators: Implement a registration form with a custom validator that ensures passwords contain at least one uppercase letter, one lowercase letter, one digit, and one special character.

  4. Error Handling: Enhance the validation error messages to provide user-friendly feedback for each field.

  5. Challenge: Create a complex form with nested objects and array validation, such as an order form with customer details and multiple order items, each with their own validation rules.

By practicing these exercises, you'll be well on your way to mastering request validation in Echo!



If you spot any mistakes on this website, please let me know at [email protected]. I’d greatly appreciate your feedback! :)