Processing requests
For the parsing, validating request and serializing response you will use Gohandlers generated helper methods. Using those methods will standardize your handlers, will bring sanity to your handler directory and enforce completeness of your implmentation with mentioning field serializers, deserializers and validators on code which will make the next compilation fail if you happen to skip defining any of them.
An handler’s anatomy
Typical handler should follow the start and end of below example. Notice how it starts with CreateRequest
literal generation, followed by calls to request parser and validator. After those your handler logic supposed to kick-in. You can access to the client provided values through the bq
(binding for request) variable in the most type-safe manner. Until that point the values are already collected, decoded and assigned to their fields whether they are a route, query or body parameter.
type CreateRequest struct {
Name types.PetName `json:"name"`
Tag types.PetTag `json:"tag"`
}
type CreateResponse struct {
ID string `json:"id"`
}
func (p *Pets) Create(w http.ResponseWriter, r *http.Request) {
bq := &CreateRequest{}
if err := bq.Parse(r); err != nil {
slog.Debug("user error on parsing request", "content", err.Error())
http.Error(w, "error on parsing request", 400)
return
}
if issues := bq.Validate(); issues != nil {
if err := serialize(w, issues); err != nil {
slog.Error("error on serialization validation error", "content", err)
http.Error(w, "error on serialization validation error", 500)
} else {
slog.Debug("user error on validation", "length", len(issues))
}
return
}
// ...
bs := &CreateResponse{
ID: "",
}
if err := bs.Write(w); err != nil {
slog.Debug("user error on serializing response", "content", err.Error())
http.Error(w, "serializing response", 400)
return
}
}
Returning validation issues
Request validator doesn’t return the first issue on whatever field was mistaken. It keeps trying the other field’s validator and collect the issues in a map. The map’s keys are the field names as they typed in the struct field tags, and the values are the issues. The issues are collected in the type field validators returned them. This design decision made to allow returning array or map type issue collections for collection fields. The returned value is ready to serialization. You can just throw it to the json
encoder and call it day.
func serialize(w http.ResponseWriter, issues map[string]any) error {
err := json.NewEncoder(w).Encode(issues)
if err != nil {
return fmt.Errorf("encoding json: %w", err)
}
return nil
}
The odds are if your client also recognizes the same identifiers as you described your fields in your struct tags; then you can automate the presentation of errors in your frontend. After all it is a JSON map now.