When building large web applications, one of the common tasks developers face is validating request body parameters and query parameters in APIs. Writing validation code for every single API can quickly become tedious and error-prone. In this tutorial, we’ll explore how to make this process easier and more efficient using Go.
By the end of this guide, you'll know how to validate request body and query parameters in Go without repetitive coding. Let’s dive in!
Step 1: Setting Up the Project
First, let's get our project set up:
git clone https://github.com/suhaasya/simple-go-server.git
After cloning, create a .env
file in your root directory:
PORT=8080 # or your preferred port
Step 2: Creating the Basic Server
Let's start with our main server file. Create cmd/api/main.go
:
package main
import (
"fmt"
"net/http"
"simple-go-server/internals/routes"
"simple-go-server/pkg/constant"
"github.com/go-chi/chi/middleware"
"github.com/go-chi/chi/v5"
)
func main() {
constant.LoadEnv()
r := chi.NewRouter()
// Basic middleware stack
r.Use(middleware.RequestID)
r.Use(middleware.RealIP)
r.Use(middleware.Logger)
r.Use(middleware.Recoverer)
r.Mount("/", routes.Router())
fmt.Println("Started the Go server on port " + constant.PORT)
http.ListenAndServe(":"+constant.PORT, r)
}
💡 This sets up a basic Chi router with some helpful middleware.
Step 3: Setting Up Routes
Create internals/routes/router.go
and internals/routes/user_routes.go
:
package routes
import (
"net/http"
"github.com/go-chi/chi"
)
func Router() http.Handler {
r := chi.NewRouter()
r.Get("/", func(w http.ResponseWriter, r *http.Request) {
w.Write([]byte("hi"))
})
r.Mount("/users", UserRoutes())
return r
}
package routes
import (
"net/http"
"simple-go-server/internals/controllers"
"github.com/go-chi/chi"
)
func UserRoutes() http.Handler {
r := chi.NewRouter()
r.Post("/", controllers.CreateUser)
r.Patch("/{id}", controllers.EditUser)
r.Get("/", controllers.GetUsers)
return r
}
💡 We've defined three routes that we'll implement one by one.
Step 4: Creating Your First Validation - POST Request
4.1: Define the Validation Structure
Create pkg/structs/user.go
:
package structs
type AddUser struct {
Name string `json:"name" validate:"required"`
Email string `json:"email" validate:"required,email"`
Password string `json:"password" validate:"required"`
Role string `json:"role" validate:"required,oneof=admin user"`
}
4.2: Create the Validation Helper
Create pkg/helper/helper.go
:
package helper
import (
"encoding/json"
"fmt"
"net/http"
"simple-go-server/pkg/constant"
"github.com/go-playground/validator"
)
func ValidateBody(r *http.Request, reqData interface{}) error {
// Decode the JSON request body into the provided struct
if err := json.NewDecoder(r.Body).Decode(reqData); err != nil {
return err
}
if fmt.Sprintf("%v", reqData) == "&{ }" {
return constant.ErrBodyCannotBeEmpty
}
// Validate the decoded data using the validator
validate := validator.New()
if err := validate.Struct(reqData); err != nil {
return err
}
// Return nil if both decoding and validation succeed
return nil
}
4.3: Implement the Create User Controller
Update internals/controllers/user_controllers.go
:
package controllers
import (
"fmt"
"net/http"
"simple-go-server/pkg/helper"
"simple-go-server/pkg/structs"
)
func CreateUser(w http.ResponseWriter, r *http.Request) {
reqData := new(structs.AddUser)
err := helper.ValidateBody(r, reqData)
if err != nil {
w.Write([]byte(err.Error()))
return
}
fmt.Println(reqData.Email)
fmt.Println(reqData.Name)
fmt.Println(reqData.Password)
fmt.Println(reqData.Role)
w.Write([]byte("User created"))
}
4.4: Testing Create User
Let's test our first endpoint:
- Try with empty body:
{}
You should get validation errors.
- Try with valid data:
{
"name": "John Doe",
"email": "john@example.com",
"password": "secret123",
"role": "user"
}
You should see the data printed in your console!
Step 5: Adding PATCH Request Validation
5.1: Add Edit User Structure
Add to pkg/structs/user.go
:
type EditUser struct {
Name string `json:"name" validate:"omitempty"`
Email string `json:"email" validate:"omitempty,email"`
Role string `json:"role" validate:"omitempty,oneof=admin user"`
}
5.2: Implement Edit User Controller
Add to internals/controllers/user_controllers.go
:
func EditUser(w http.ResponseWriter, r *http.Request) {
reqData := new(structs.EditUser)
err := helper.ValidateBody(r, reqData)
if err != nil {
w.Write([]byte(err.Error()))
return
}
if reqData.Email != "" {
fmt.Println(reqData.Email)
}
if reqData.Name != "" {
fmt.Println(reqData.Name)
}
if reqData.Role != "" {
fmt.Println(reqData.Role)
}
w.Write([]byte("User edited"))
}
5.3: Testing Edit User
Try these test cases:
- Partial update:
{
"name": "Jane Doe"
}
- Invalid email:
{
"email": "not-an-email"
}
Step 6: Adding Query Parameter Validation
6.1: Add Query Parameters Structure
Add to pkg/structs/user.go
:
type QueryParams struct {
Page string `json:"page" validate:"omitempty,numeric,min=1"`
Size string `json:"size" validate:"omitempty,numeric,min=1"`
Order string `json:"order" validate:"omitempty,oneof=asc desc"`
OrderBy string `json:"orderBy" validate:"omitempty,oneof=id name"`
}
6.2: Add Query Parameter Helpers
Add to pkg/helper/helper.go
:
func QueryToJSON(query url.Values) (string, error) {
// Create a map to store our processed query parameters
queryMap := make(map[string]interface{})
// Iterate through all query parameters
for key, values := range query {
// If there's only one value, store it directly
if len(values) == 1 {
queryMap[key] = values[0]
} else {
// If there are multiple values, store them as an array
queryMap[key] = values
}
}
// Convert map to JSON
jsonBytes, err := json.Marshal(queryMap)
if err != nil {
return "", err
}
return string(jsonBytes), nil
}
func ValidateQueryParams(jsonData []byte, reqData interface{}) error {
// Decode the JSON request body into the provided struct
err := json.Unmarshal(jsonData, reqData)
if err != nil {
return err
}
// Validate the decoded data using the validator
validate := validator.New()
if err := validate.Struct(reqData); err != nil {
return err
}
// Return nil if both decoding and validation succeed
return nil
}
6.3: Implement Get Users Controller
Add to internals/controllers/user_controllers.go
:
func GetUsers(w http.ResponseWriter, r *http.Request) {
query := r.URL.Query()
queryJson, _ := helper.QueryToJSON(query)
queryParams := new(structs.QueryParams)
err := helper.ValidateQueryParams([]byte(queryJson), queryParams)
if err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
if queryParams.Page != "" {
fmt.Println(queryParams.Page)
}
if queryParams.Order != "" {
fmt.Println(queryParams.Order)
}
if queryParams.OrderBy != "" {
fmt.Println(queryParams.OrderBy)
}
if queryParams.Size != "" {
fmt.Println(queryParams.Size)
}
w.Write([]byte("User fetched"))
}
6.4: Testing Query Parameters
Try these URLs:
Valid:
/users?page=1&size=10&order=asc&orderBy=name
Invalid:
/users?page=a&size=10
(page not num)Invalid:
/users?order=random
(invalid order value)
Conclusion
Congratulations! 🎉 You've built a complete validation system for your Go API. We:
Started with basic request body validation
Added support for optional fields in PATCH requests
Finished with query parameter validation
The best part? This system is completely reusable! Just create new structs with your validation rules, and you're good to go.