JSON Responses
Welcome to the Tasks API. If you've done Fundamentals' "Building HTTP
Services" chapter, you already know your way around http.ResponseWriter,
*http.Request, and http.ServeMux — this book doesn't re-teach any of
that. What it teaches is how those pieces come together into a real,
JSON-speaking service, one lesson at a time. This first lesson builds the
smallest, most-used piece of plumbing in the whole project: two functions
that turn a Go value into an HTTP response.
Why one helper, not fifty Fprintf calls
Every handler you write from here on needs to send JSON back to the client —
a task, a list of tasks, an error. You could call json.NewEncoder(w).Encode(v)
directly in each handler, but then every handler also has to remember to set
Content-Type, remember to set it before writing, and pick a status code
consistently. Multiply that by a dozen handlers over a dozen lessons and
you've got a dozen chances to get it slightly wrong — one handler forgets
the header, another sets the status after the body, another returns a
different error shape than the rest.
The fix is to write it once. writeJSON and writeError are the only two
functions in this service that touch response encoding directly. Every
handler you build later calls them instead of touching w by hand. When you
want to change something about how JSON goes out — say, adding a
X-Request-ID header everywhere — you change it in one place, and every
endpoint gets it for free.
The ordering rule
http.ResponseWriter only lets you set headers before the status line is
sent, and the status line is sent the moment you call WriteHeader — or the
moment you call Write for the first time, whichever comes first. That
gives you exactly one correct order:
- Set headers (
w.Header().Set(...)) - Call
w.WriteHeader(status) - Write the body
Reverse steps 1 and 2 — call WriteHeader and then try to set a header —
and the header silently doesn't apply; the response has already committed
to whatever headers existed at that point. This isn't a Go quirk so much as
how HTTP itself works: the status line and headers form a fixed preamble
that has to go out before any body bytes, so once bytes start flowing there's
nothing left to amend. writeJSON encodes this rule as code so you never
have to think about the order again — get the three lines right once, and
every caller inherits the correct behavior.
Encode, not Marshal
Use json.NewEncoder(w).Encode(v) rather than json.Marshal followed by
w.Write. Both work, but Encode writes straight to the io.Writer you
already have — no need to hold the whole payload in a byte slice first — and
it appends a trailing newline after the JSON value. That newline is harmless
for API clients (any JSON decoder ignores trailing whitespace) and makes raw
response bodies pleasant to read with curl or in a terminal, so it's the
idiomatic choice for HTTP handlers.
One error shape for the whole service
writeError doesn't do any encoding of its own — it just calls writeJSON
with map[string]string{"error": msg}. That's deliberate. A client
integrating against this API — whether it's a frontend, a script, or another
service — only has to learn one error shape: {"error": "<message>"}. It
doesn't matter whether the error came from a validation failure, a missing
task, or something else entirely; the client can always look in the same
place. Compare that to an API where a 404 returns plain text and a 400
returns a different JSON structure — every consumer ends up writing
special-case parsing for each endpoint. Predictability here is worth more
than expressiveness.
These two functions are barely ten lines of code together, but they're the
foundation the rest of this book stands on. Every handler for creating,
listing, updating, and deleting tasks — plus every error path in between —
funnels its response through writeJSON and writeError. Get them right
now and you won't touch response encoding again for the rest of the course.
Practice
1. JSON Responses
IntermediateEvery endpoint in this service returns JSON, so the first thing to build is a pair of helpers you'll reuse for the rest of the book.
writeJSON(w http.ResponseWriter, status int, v any)— setContent-Type: application/json, write the status code, then encodevto the body. Order matters: set the header beforeWriteHeader, andWriteHeaderbefore the first body write.writeError(w http.ResponseWriter, status int, msg string)— a consistent error shape. Just callwriteJSONwithmap[string]string{"error": msg}so every error looks the same on the wire.
The hidden tests drive both with
httptest.NewRecorderand decode the result.package main import ( "net/http" ) // writeJSON writes v as a JSON response with the given status code. Set the // Content-Type header before WriteHeader — the first write locks headers in. func writeJSON(w http.ResponseWriter, status int, v any) { // TODO: set Content-Type to "application/json", call w.WriteHeader(status), // then encode v to w with encoding/json. } // writeError writes a consistent JSON error envelope: {"error": msg}. func writeError(w http.ResponseWriter, status int, msg string) { // TODO: delegate to writeJSON with map[string]string{"error": msg}. } func main() {}Sign in to write and run your solution in the browser.
Sign in for hints and the solution.