package validate
import (
"regexp"
"unicode/utf8"
)
var EmailRX = regexp.MustCompile(`^[a-zA-Z0-9._%+\-]+@[a-zA-Z0-9.\-]+\.[a-zA-Z]{2,}$`)
type Validator struct {
FieldErrors map[string]string `json:"field_errors"`
}
func New() *Validator {
return &Validator{FieldErrors: map[string]string{}}
}
func (v *Validator) Valid() bool {
return len(v.FieldErrors) == 0
}
func (v *Validator) AddError(field, message string) {
if _, exists := v.FieldErrors[field]; !exists {
v.FieldErrors[field] = message
}
}
func (v *Validator) Check(ok bool, field, message string) {
if !ok {
v.AddError(field, message)
}
}
func NotBlank(value string) bool {
return value != ""
}
func MinChars(value string, n int) bool {
return utf8.RuneCountInString(value) >= n
}
func MaxChars(value string, n int) bool {
return utf8.RuneCountInString(value) <= n
}
func Matches(value string, rx *regexp.Regexp) bool {
return rx.MatchString(value)
}
package signup
import (
"encoding/json"
"net/http"
"strings"
"example.com/app/validate"
)
type SignupForm struct {
Name string `json:"name"`
Email string `json:"email"`
Password string `json:"password"`
PasswordConfirm string `json:"password_confirm"`
}
func SignupHandler(w http.ResponseWriter, r *http.Request) {
var form SignupForm
dec := json.NewDecoder(r.Body)
dec.DisallowUnknownFields()
if err := dec.Decode(&form); err != nil {
v := validate.New()
v.AddError("", "request body is not valid JSON")
writeJSON(w, http.StatusBadRequest, v)
return
}
form.Name = strings.TrimSpace(form.Name)
form.Email = strings.TrimSpace(form.Email)
v := validate.New()
v.Check(validate.NotBlank(form.Name), "name", "Name is required")
v.Check(validate.MaxChars(form.Name, 100), "name", "Name must be 100 characters or fewer")
v.Check(validate.NotBlank(form.Email), "email", "Email is required")
v.Check(validate.Matches(form.Email, validate.EmailRX), "email", "Enter a valid email address")
v.Check(validate.NotBlank(form.Password), "password", "Password is required")
v.Check(validate.MinChars(form.Password, 8), "password", "Password must be at least 8 characters")
v.Check(form.Password == form.PasswordConfirm, "password_confirm", "Passwords do not match")
if !v.Valid() {
writeJSON(w, http.StatusUnprocessableEntity, v)
return
}
writeJSON(w, http.StatusCreated, map[string]string{"status": "account created"})
}
func writeJSON(w http.ResponseWriter, status int, payload interface{}) {
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(status)
_ = json.NewEncoder(w).Encode(payload)
}
This snippet shows how a Go HTTP service validates a signup form and returns structured, per-field errors instead of a single opaque message. The goal is a JSON shape a frontend can bind directly to input fields, so each rule failure is attached to the name of the field it concerns.
In validator.go, Validator is a tiny accumulator around a map[string]string keyed by field name. Its Check method records a message only when a condition is false and only if that field has no error yet, which means the first failing rule per field wins and messages don't get clobbered. Keeping one message per field is a deliberate trade-off: it keeps the UI simple at the cost of not reporting every possible problem at once. Valid reports whether any errors accumulated, and helpers like NotBlank, MinChars, and Matches are pure predicates so the rules read declaratively at the call site. EmailRX is compiled once at package load with regexp.MustCompile to avoid recompiling on every request.
In handler.go, SignupHandler decodes the request body into a SignupForm. json.NewDecoder with DisallowUnknownFields rejects unexpected keys early, guarding against typos and stale clients. The handler then constructs a Validator and expresses every business rule through v.Check: presence, length bounds, email format via Matches(EmailRX, ...), and a cross-field rule confirming Password equals PasswordConfirm. This separation matters — the handler owns the policy (which rules apply) while Validator owns the mechanics of collecting failures.
When v.Valid() is false, writeJSON returns HTTP 422 Unprocessable Entity with the FieldErrors map, the conventional status for a well-formed request that fails semantic validation. A generic "" key carries non-field errors such as a malformed JSON body.
The pattern scales well: new rules are one-liners, the response contract stays stable, and the validator has no dependency on net/http, so the same logic can back a CLI or a gRPC layer. A pitfall to watch is trusting client-side checks; server-side validation here is authoritative. It also normalizes input with strings.TrimSpace before checking, so whitespace-only fields are correctly treated as blank.
Related snips
package api
import (
"net/http"
"runtime/debug"
)
Expose build metadata for debugging deploys
export interface RetryOptions {
retries: number;
baseMs: number;
maxMs: number;
signal?: AbortSignal;
onRetry?: (attempt: number, delay: number, err: unknown) => void;
Exponential backoff with jitter for retries
package dbutil
import (
"context"
"github.com/jackc/pgconn"
Retry Postgres serialization failures with bounded attempts
class SignupForm
include ActiveModel::Model
include ActiveModel::Attributes
attribute :account_name, :string
attribute :email, :string
Shallow Controller, Deep Params: Form Object Pattern
// Creating a Promise
const myPromise = new Promise((resolve, reject) => {
const success = true;
setTimeout(() => {
if (success) {
Promises and async/await patterns for asynchronous JavaScript
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Form Validation Example</title>
<style>
HTML forms with validation and accessibility
Share this code
Here's the card — post it anywhere.