JSON APIs
You already know how to marshal and unmarshal JSON from the JSON in Depth
chapter, and Lesson 1 gave you ResponseWriter and *Request. This lesson
puts the two together: reading a JSON body in, writing a JSON body out,
inside a plain net/http handler.
The body is a reader, not a string
r.Body is an io.ReadCloser. The tempting move is to io.ReadAll(r.Body)
and then json.Unmarshal the result — and it works — but it means holding
the entire request in memory before you've looked at a single byte of it.
Prefer the streaming decoder instead:
var req sumRequest
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
http.Error(w, "invalid JSON body", http.StatusBadRequest)
return
}
json.NewDecoder wraps any io.Reader and decodes directly from the
stream, so the body is read once, incrementally, into req. For request
bodies — which are almost always modest and read exactly once — this is the
idiomatic default. Decode returns an error for malformed JSON, wrong
types, and truncated input, which is exactly the condition you want to turn
into a 400 Bad Request.
Encoding the response the same way
The mirror image is json.NewEncoder:
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(sumResponse{Sum: total})
Encode marshals its argument and writes it straight to w, no
intermediate []byte. One quirk worth knowing: Encoder.Encode appends a
trailing newline after the JSON value (unlike json.Marshal, which
doesn't). It's harmless for API clients — they parse the value and ignore
trailing whitespace — but it's why a byte-for-byte comparison against
json.Marshal output will be off by one byte.
Headers before body, always
w.Header().Set(...) only has an effect if it runs before the first call
to Write (which Encode triggers). Once a byte hits the wire, Go has
already sent the status line and headers — using whatever was set at that
point, defaulting the status to 200 OK if you never called
WriteHeader. So the order in the snippet above isn't a style choice: set
Content-Type, then encode. Reverse it and the header silently never
makes it into the response.
The same rule governs status codes. http.Error(w, msg, code) is a
convenience that sets Content-Type: text/plain, calls w.WriteHeader(code),
then writes msg — one call covers the header-then-body ordering for you.
For success responses with a body, you generally don't need to call
w.WriteHeader explicitly: writing triggers an implicit 200. When you do
need a specific success code — http.StatusCreated for a resource that was
just made, for instance — call w.WriteHeader(code) yourself before
writing, in that order.
Guarding the method by hand
Lesson 1's ServeMux patterns ("POST /sum") get you automatic 405s for
free. Plenty of real handlers, though — especially ones registered under a
single path for multiple concerns, or wrapped in middleware — check the
method themselves:
if r.Method != http.MethodPost {
http.Error(w, "method not allowed", http.StatusMethodNotAllowed)
return
}
This is the manual version of what the mux pattern does automatically. Both are common in real code; know both.
Putting it together: a POST-echo handler
type echoRequest struct {
Message string `json:"message"`
}
type echoResponse struct {
Echo string `json:"echo"`
}
func EchoHandler(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodPost {
http.Error(w, "method not allowed", http.StatusMethodNotAllowed)
return
}
var req echoRequest
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
http.Error(w, "invalid JSON body", http.StatusBadRequest)
return
}
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(echoResponse{Echo: req.Message})
}
Four steps, in order: guard the method, decode the request, set headers, encode the response. The exercise asks you to write the same shape for a handler that sums a list of numbers instead of echoing a string — the skeleton is identical.
Practice
1. A JSON Endpoint
IntermediateJSON in, JSON out — the shape of most Go web endpoints. Implement
SumHandler(w http.ResponseWriter, r *http.Request):- Only
POSTis allowed. Any other method →405 Method Not Allowed. - The body is
{"numbers":[1,2,3]}. Decode it; if it doesn't parse, respond400 Bad Request. - On success, reply
{"sum":6}withContent-Type: application/jsonand status 200. An empty list sums to 0.
Decode straight from the body with
json.NewDecoder(r.Body).Decode(&req)and write the reply withjson.NewEncoder(w).Encode(resp)— no need to buffer the whole payload. Set theContent-Typeheader before you encode (writing the body locks the status and headers in).http.Error(w, msg, code)is the one-liner for the 400 and 405 paths.package main import ( "net/http" ) type sumRequest struct { Numbers []int `json:"numbers"` } type sumResponse struct { Sum int `json:"sum"` } // SumHandler accepts POST {"numbers":[...]} and replies {"sum":N}. It rejects // non-POST with 405 and unparseable bodies with 400. func SumHandler(w http.ResponseWriter, r *http.Request) { // TODO: reject non-POST with 405; decode the body into sumRequest, // 400 on error; sum the numbers and encode {"sum":N} with // Content-Type application/json. } func main() { mux := http.NewServeMux() mux.HandleFunc("/sum", SumHandler) http.ListenAndServe(":8080", mux) }Sign in to write and run your solution in the browser.
Sign in for hints and the solution.
- Only