go
57 lines · 9 steps
Building reusable JSON helpers in Go HTTP handlers
A small set of helpers centralize JSON encoding and error responses so handlers stay focused on business logic.
Explained by
highlit
1package api
2
3import (
4 "encoding/json"
5 "log"
6 "net/http"
7)
8
9type ErrorBody struct {
10 Error string `json:"error"`
11 Details string `json:"details,omitempty"`
12}
13
14func WriteJSON(w http.ResponseWriter, status int, payload any) {
15 buf, err := json.Marshal(payload)
16 if err != nil {
17 log.Printf("api: failed to marshal response: %v", err)
18 w.Header().Set("Content-Type", "application/json; charset=utf-8")
19 w.WriteHeader(http.StatusInternalServerError)
20 w.Write([]byte(`{"error":"internal server error"}`))
21 return
22 }
23
24 w.Header().Set("Content-Type", "application/json; charset=utf-8")
25 w.Header().Set("X-Content-Type-Options", "nosniff")
26 w.Header().Set("Cache-Control", "no-store")
27 w.WriteHeader(status)
28 w.Write(buf)
29}
30
31func WriteError(w http.ResponseWriter, status int, msg, details string) {
32 WriteJSON(w, status, ErrorBody{Error: msg, Details: details})
33}
34
35func GetUser(store UserStore) http.HandlerFunc {
36 return func(w http.ResponseWriter, r *http.Request) {
37 id := r.PathValue("id")
38 if id == "" {
39 WriteError(w, http.StatusBadRequest, "missing user id", "")
40 return
41 }
42
43 user, err := store.FindByID(r.Context(), id)
44 switch {
45 case err == ErrNotFound:
46 WriteError(w, http.StatusNotFound, "user not found", id)
47 return
48 case err != nil:
49 log.Printf("api: lookup failed for %s: %v", id, err)
50 WriteError(w, http.StatusInternalServerError, "failed to load user", "")
51 return
52 }
53
54 w.Header().Set("ETag", user.ETag())
55 WriteJSON(w, http.StatusOK, user)
56 }
57}
01 / 01
STEP 01
‹ swipe to step through ›
Walkthrough
Space play
←→ step
click any line
Three takeaways
- 1Centralizing serialization in one helper guarantees consistent headers and status codes across every handler.
- 2Returning a closure from a constructor function lets handlers capture dependencies like a data store cleanly.
- 3Distinguishing not-found from unexpected errors lets you map each to the correct HTTP status.
Related explainers
rust
use serde::Deserialize; #[derive(Debug, Deserialize)] #[serde(untagged)]
Parsing flexible JSON shapes with serde
deserialization
enums
json
Intermediate
6 steps
ruby
require "shellwords" require "open3" module Backup
Building safe shell commands in Ruby
shell-out
subprocess
command-injection
Intermediate
7 steps
go
package streaming import ( "bufio"
Streaming NDJSON logs over HTTP in Go
http-streaming
channels
select
Advanced
10 steps
javascript
function evaluate(expression) { const tokens = tokenize(expression); let pos = 0;
Building a recursive descent calculator
parsing
recursion
operator-precedence
Intermediate
8 steps
go
package api import ( "crypto/sha256"
ETag conditional requests in Gin
http-caching
etag
conditional-requests
Intermediate
6 steps
go
func (w *Watcher) resetDebounce(d time.Duration) { if !w.timer.Stop() { select { case <-w.timer.C:
Debouncing a stream of events in Go
debounce
timers
channels
Advanced
7 steps
Share this explainer
Here's the card — post it anywhere.
Made with highlit — turn any snippet into a walkthrough like this in about a minute.
Explain your code
Embed this explainer
Drop the interactive walkthrough into a blog or docs. Views never cost a credit.
<iframe src="https://highlit.co/explainers/building-reusable-json-helpers-in-go-http-handlers-explained-go-47f1/embed?autoplay=1" width="100%" height="520" loading="lazy" style="border:0"></iframe>
Autoplay is on by default — add ?autoplay=0 to start paused.