Kinoko's TIL Log

API Request Validation

The Point

API request validation should happen on both sides, but with different roles: the client provides instant UX feedback, while the server enforces all business logic validation. The server side is mandatory because client validation can be bypassed.

Explanation

Why can’t you skip server-side validation?

Anyone can skip the frontend and hit the API directly. Client-side validation is just a UX optimization; the server is the real enforcement layer.

Cross-field Validation

When fields have logical dependencies (e.g. discount cannot be 0 when eligibility is true), handle it in a Validate() method on the server side:

 1type CreateDiscountRequest struct {
 2    Eligibility bool    `json:"eligibility"`
 3    Discount    float64 `json:"discount"`
 4}
 5
 6func (r CreateDiscountRequest) Validate() error {
 7    if r.Eligibility && r.Discount == 0 {
 8        return errors.New("discount cannot be 0 when eligibility is true")
 9    }
10    return nil
11}

Call it in the handler right after decoding:

1var req CreateDiscountRequest
2json.NewDecoder(r.Body).Decode(&req)
3
4if err := req.Validate(); err != nil {
5    http.Error(w, err.Error(), http.StatusBadRequest) // 400
6    return
7}

Knowledge Sugar

Should you use a validation library?

For simple cases, a hand-written Validate() is enough. When rules get complex or you have many structs, consider go-playground/validator, which supports struct tags for basic rules:

1type Request struct {
2    Name  string  `validate:"required"`
3    Email string  `validate:"required,email"`
4    Age   int     `validate:"gte=0,lte=130"`
5}

But cross-field logic (dependencies between fields) still needs a custom validator – no library handles your business logic for you.

Return 400 or 422?

Semantically 422 is more precise, but in practice many APIs use 400 for everything. Follow your team’s convention.

#api-design #til

← Back to Main Page