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:
- Security: Prevents malicious data from causing vulnerabilities
- Data integrity: Ensures your application works with valid and properly formatted data
- Error prevention: Reduces runtime errors caused by unexpected inputs
- 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:
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:
go get github.com/go-playground/validator/v10
Then integrate it with Echo:
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
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
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:
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:
Tag | Description | Example |
---|---|---|
required | Field must not be empty | validate:"required" |
email | Field must be a valid email | validate:"email" |
min | Minimum length for strings or minimum value for numbers | validate:"min=3" |
max | Maximum length for strings or maximum value for numbers | validate:"max=100" |
len | Exact length for strings or exact value for numbers | validate:"len=10" |
oneof | Field value must be one of the given values | validate:"oneof=male female other" |
gt/gte | Greater than/Greater than or equal | validate:"gte=18" |
lt/lte | Less than/Less than or equal | validate:"lt=130" |
numeric | Field must be numeric | validate:"numeric" |
alphanum | Field must contain only alphanumeric characters | validate:"alphanum" |
Custom Validation Functions
Sometimes you need validation logic not covered by built-in rules. Let's implement a custom validation function:
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:
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:
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:
{
"username": "john123",
"email": "invalid-email",
"password": "password",
"confirm_password": "different",
"first_name": "John",
"last_name": "Doe",
"age": 16,
"country": "USA",
"accept_terms": false
}
Response:
{
"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
- Validate on the server side: Never rely solely on client-side validation
- Validate all input types: Request body, path parameters, query parameters, and headers
- Use appropriate HTTP status codes: 400 Bad Request for validation errors
- Provide clear error messages: Help users understand what went wrong
- Consider security implications: Validate input length to prevent DoS attacks
- Sanitize input: Besides validation, consider sanitizing input to prevent XSS attacks
- Use strict type checking: Ensure numeric fields contain valid numbers
- 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
- Echo Framework Documentation
- go-playground/validator Documentation
- OWASP Input Validation Cheat Sheet
Exercises
- 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).
- Implement a custom validation function that validates a credit card number using the Luhn algorithm.
- Build an address validation endpoint that checks postal codes against a specific country's format.
- 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! :)