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
ruby
class Order class InvalidTransition < StandardError; end TRANSITIONS = {
A state machine for order transitions in Ruby
state-machine
data-driven
error-handling
Intermediate
8 steps
go
package theme import ( "fmt"
Per-tenant HTML templates with Gin's renderer
multi-tenancy
concurrency
html-templates
Advanced
8 steps
go
package auth import ( "net/http"
Setting and reading secure session cookies in Go
cookies
session-management
security
Intermediate
6 steps
rust
use std::cmp::Ordering; use std::str::FromStr; #[derive(Debug, Clone, PartialEq, Eq)]
Parsing and ordering semantic versions in Rust
parsing
trait-implementation
ordering
Intermediate
8 steps
javascript
'use client'; import { useEffect } from 'react'; import * as Sentry from '@sentry/nextjs';
How a Next.js error boundary recovers
error-boundary
error-handling
observability
Intermediate
8 steps
ruby
class Registration < ApplicationRecord belongs_to :event validates :email, presence: true, format: { with: URI::MailTo::EMAIL_REGEXP }
Validating registrations in Rails
validations
i18n
error-handling
Intermediate
8 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.