Skip to main content

Echo Input Validation

Introduction

Input validation is a critical aspect of API development that helps ensure the data your application receives meets expected criteria before processing. For APIs built with Echo framework, implementing validation is essential for several reasons:

  1. Security: Prevents malicious data from causing vulnerabilities
  2. Data integrity: Ensures your application works with valid and properly formatted data
  3. Error prevention: Reduces runtime errors caused by unexpected inputs
  4. User experience: Provides clear feedback about why a request was rejected

In this guide, we'll explore how to implement robust input validation in Echo API endpoints, from basic techniques to more advanced validation patterns.

Basic Input Validation

Manual Validation

The simplest form of input validation is checking values directly in your handler functions:

go
package main

import (
"net/http"
"github.com/labstack/echo/v4"
)

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

e.POST("/users", createUser)

e.Start(":8080")
}

type User struct {
Name string `json:"name"`
Email string `json:"email"`
Age int `json:"age"`
}

func createUser(c echo.Context) error {
u := new(User)
if err := c.Bind(u); err != nil {
return c.JSON(http.StatusBadRequest, map[string]string{
"error": "Invalid request body",
})
}

// Basic validation
if u.Name == "" {
return c.JSON(http.StatusBadRequest, map[string]string{
"error": "Name is required",
})
}

if u.Email == "" {
return c.JSON(http.StatusBadRequest, map[string]string{
"error": "Email is required",
})
}

if u.Age <= 0 {
return c.JSON(http.StatusBadRequest, map[string]string{
"error": "Age must be a positive number",
})
}

// Process the validated data
return c.JSON(http.StatusCreated, u)
}

While this approach works for simple cases, it becomes cumbersome for complex validation requirements and doesn't scale well across multiple handlers.

Using Validator Library

Echo integrates seamlessly with the popular go-playground/validator package, which provides struct-based validation using tags.

Step 1: Setting up the validator

First, install the validator package:

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

Then integrate it with Echo:

go
package main

import (
"net/http"
"github.com/labstack/echo/v4"
"github.com/go-playground/validator/v10"
)

type CustomValidator struct {
validator *validator.Validate
}

func (cv *CustomValidator) Validate(i interface{}) error {
return cv.validator.Struct(i)
}

func main() {
e := echo.New()
e.Validator = &CustomValidator{validator: validator.New()}

e.POST("/users", createUser)

e.Start(":8080")
}

Step 2: Define validation rules using struct tags

go
type User struct {
Name string `json:"name" validate:"required"`
Email string `json:"email" validate:"required,email"`
Age int `json:"age" validate:"required,gt=0"`
Phone string `json:"phone" validate:"omitempty,e164"`
}

Step 3: Implement validation in handlers

go
func createUser(c echo.Context) error {
u := new(User)
if err := c.Bind(u); err != nil {
return c.JSON(http.StatusBadRequest, map[string]string{
"error": "Invalid request body",
})
}

if err := c.Validate(u); err != nil {
return c.JSON(http.StatusBadRequest, map[string]string{
"error": err.Error(),
})
}

// Process the validated user data
return c.JSON(http.StatusCreated, u)
}

Improving Validation Error Responses

Default error messages from validators aren't always user-friendly. Let's create better error responses:

go
func createUser(c echo.Context) error {
u := new(User)
if err := c.Bind(u); err != nil {
return c.JSON(http.StatusBadRequest, map[string]string{
"error": "Invalid request format",
})
}

if err := c.Validate(u); err != nil {
// Cast the error to validator.ValidationErrors
if valErrs, ok := err.(validator.ValidationErrors); ok {
errors := make(map[string]string)
for _, e := range valErrs {
field := e.Field()
switch e.Tag() {
case "required":
errors[field] = field + " is required"
case "email":
errors[field] = field + " should be a valid email address"
case "gt":
errors[field] = field + " should be greater than " + e.Param()
default:
errors[field] = "Invalid " + field
}
}
return c.JSON(http.StatusBadRequest, map[string]interface{}{
"errors": errors,
})
}

return c.JSON(http.StatusBadRequest, map[string]string{
"error": "Validation failed",
})
}

// Process the validated user data
return c.JSON(http.StatusCreated, u)
}

Common Validation Rules

Here are some frequently used validation tags:

TagDescriptionExample
requiredField must not be emptyvalidate:"required"
emailField must be a valid emailvalidate:"email"
minMinimum length for strings or minimum value for numbersvalidate:"min=3"
maxMaximum length for strings or maximum value for numbersvalidate:"max=100"
lenExact length for strings or exact value for numbersvalidate:"len=10"
oneofField value must be one of the given valuesvalidate:"oneof=male female other"
gt/gteGreater than/Greater than or equalvalidate:"gte=18"
lt/lteLess than/Less than or equalvalidate:"lt=130"
numericField must be numericvalidate:"numeric"
alphanumField must contain only alphanumeric charactersvalidate:"alphanum"

Custom Validation Functions

Sometimes you need validation logic not covered by built-in rules. Let's implement a custom validation function:

go
package main

import (
"github.com/go-playground/validator/v10"
"github.com/labstack/echo/v4"
"net/http"
"regexp"
)

type CustomValidator struct {
validator *validator.Validate
}

func (cv *CustomValidator) Validate(i interface{}) error {
return cv.validator.Struct(i)
}

type Product struct {
ID string `json:"id" validate:"required,product_id"`
Name string `json:"name" validate:"required"`
Description string `json:"description" validate:"max=500"`
Price float64 `json:"price" validate:"required,gt=0"`
}

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

// Register custom validation function
v.RegisterValidation("product_id", validateProductID)

e.Validator = &CustomValidator{validator: v}

e.POST("/products", createProduct)

e.Start(":8080")
}

// Custom validation function for product ID
func validateProductID(fl validator.FieldLevel) bool {
// Product ID must be in the format PRD-XXXX where X is alphanumeric
pattern := `^PRD-[A-Z0-9]{4}$`
regex := regexp.MustCompile(pattern)
return regex.MatchString(fl.Field().String())
}

func createProduct(c echo.Context) error {
p := new(Product)
if err := c.Bind(p); err != nil {
return c.JSON(http.StatusBadRequest, map[string]string{
"error": "Invalid request body",
})
}

if err := c.Validate(p); err != nil {
return c.JSON(http.StatusBadRequest, map[string]string{
"error": err.Error(),
})
}

// Process the validated product
return c.JSON(http.StatusCreated, p)
}

Path and Query Parameter Validation

Besides request body validation, it's also important to validate path and query parameters:

go
func getUser(c echo.Context) error {
// Get ID from path param
id := c.Param("id")

// Validate ID format
if match, _ := regexp.MatchString(`^[1-9]\d*$`, id); !match {
return c.JSON(http.StatusBadRequest, map[string]string{
"error": "Invalid user ID format",
})
}

// Get and validate query parameter
role := c.QueryParam("role")
if role != "" && role != "admin" && role != "user" && role != "guest" {
return c.JSON(http.StatusBadRequest, map[string]string{
"error": "Invalid role parameter",
})
}

// Continue processing request with validated parameters
// ...

return c.JSON(http.StatusOK, map[string]string{"message": "Success"})
}

Real-World Example: API Registration Form

Let's build a complete example of a user registration API with comprehensive validation:

go
package main

import (
"github.com/labstack/echo/v4"
"github.com/go-playground/validator/v10"
"net/http"
"strings"
)

type CustomValidator struct {
validator *validator.Validate
}

func (cv *CustomValidator) Validate(i interface{}) error {
return cv.validator.Struct(i)
}

type RegistrationRequest struct {
Username string `json:"username" validate:"required,alphanum,min=4,max=20"`
Email string `json:"email" validate:"required,email"`
Password string `json:"password" validate:"required,min=8,containsany=!@#$%^&*"`
ConfirmPassword string `json:"confirm_password" validate:"required,eqfield=Password"`
FirstName string `json:"first_name" validate:"required"`
LastName string `json:"last_name" validate:"required"`
Age int `json:"age" validate:"required,gte=18"`
Country string `json:"country" validate:"required,iso3166_1_alpha2"`
AcceptTerms bool `json:"accept_terms" validate:"required,eq=true"`
}

// Helper function to create user-friendly error messages
func translateValidationError(err validator.FieldError) string {
field := strings.ToLower(err.Field())

switch err.Tag() {
case "required":
return field + " is required"
case "email":
return "please enter a valid email address"
case "min":
return field + " must be at least " + err.Param() + " characters long"
case "max":
return field + " must not be longer than " + err.Param() + " characters"
case "eqfield":
return "passwords do not match"
case "gte":
return field + " must be at least " + err.Param()
case "alphanum":
return field + " must contain only alphanumeric characters"
case "containsany":
return field + " must contain at least one special character (" + err.Param() + ")"
case "iso3166_1_alpha2":
return "please enter a valid country code"
case "eq":
return "you must accept the terms and conditions"
default:
return "validation error on field " + field
}
}

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

// Initialize validator
v := validator.New()
e.Validator = &CustomValidator{validator: v}

// Registration endpoint
e.POST("/register", registerUser)

e.Start(":8080")
}

func registerUser(c echo.Context) error {
req := new(RegistrationRequest)

// Bind request body
if err := c.Bind(req); err != nil {
return c.JSON(http.StatusBadRequest, map[string]string{
"error": "Invalid request format",
})
}

// Validate the request
if err := c.Validate(req); err != nil {
if valErrs, ok := err.(validator.ValidationErrors); ok {
// Create friendly error messages
errors := make(map[string]string)
for _, e := range valErrs {
errors[strings.ToLower(e.Field())] = translateValidationError(e)
}
return c.JSON(http.StatusBadRequest, map[string]interface{}{
"errors": errors,
})
}

return c.JSON(http.StatusBadRequest, map[string]string{
"error": "Validation failed",
})
}

// If validation passes, process registration
// (In a real application, you would hash the password and store the user data)

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

Example request and response:

Request:

json
{
"username": "john123",
"email": "invalid-email",
"password": "password",
"confirm_password": "different",
"first_name": "John",
"last_name": "Doe",
"age": 16,
"country": "USA",
"accept_terms": false
}

Response:

json
{
"errors": {
"accept_terms": "you must accept the terms and conditions",
"age": "age must be at least 18",
"confirm_password": "passwords do not match",
"country": "please enter a valid country code",
"email": "please enter a valid email address",
"password": "password must contain at least one special character (!@#$%^&*)",
"password": "password must be at least 8 characters long"
}
}

Best Practices for Input Validation

  1. Validate on the server side: Never rely solely on client-side validation
  2. Validate all input types: Request body, path parameters, query parameters, and headers
  3. Use appropriate HTTP status codes: 400 Bad Request for validation errors
  4. Provide clear error messages: Help users understand what went wrong
  5. Consider security implications: Validate input length to prevent DoS attacks
  6. Sanitize input: Besides validation, consider sanitizing input to prevent XSS attacks
  7. Use strict type checking: Ensure numeric fields contain valid numbers
  8. Apply contextual validation: Some fields may have validation rules that depend on other fields

Summary

Input validation is a critical component of building secure, reliable APIs with Echo. By implementing robust validation:

  • You protect your application from malicious or invalid data
  • You provide a better developer experience for API consumers through clear error messages
  • You reduce the risk of runtime errors and unexpected behavior
  • You create a more maintainable codebase by centralizing validation logic

The validator package combined with Echo provides a powerful, flexible system for implementing input validation in your API endpoints. Starting with basic validation tags and advancing to custom validation functions allows you to handle any validation scenario your application requires.

Additional Resources

Exercises

  1. Create an Echo API endpoint for a blog post with validation for title (required, max 100 chars), content (required), and tags (array, each tag max 20 chars).
  2. Implement a custom validation function that validates a credit card number using the Luhn algorithm.
  3. Build an address validation endpoint that checks postal codes against a specific country's format.
  4. Add validation to an existing API that ensures dates are in ISO-8601 format and not in the past.


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