Day 107 — Project: Handlers & Services¶
Month 4 · Week 4 · ⬅ Day 106 · Day 108 ➡ · Journal index
🎯 Learning Objective¶
Implement the transport and business layers of the capstone: thin handlers that decode/encode JSON and a service that holds the actual rules, talking only to the repository interface.
📚 Topics¶
- Handler responsibilities: decode → call service → encode; nothing else
- Service responsibilities: validation, normalisation, orchestration
- JSON helpers (
writeJSON/decodeJSON) and request-body limits - Constructor injection (
NewService,NewHandler)
📖 Reading / Sources¶
-
encoding/jsonpackage docs -
net/httpServeMux (Go 1.22 routing) - Mat Ryer — How I write HTTP services in Go (2024)
📝 Notes¶
- A handler is a translator, not a brain. It decodes the request, calls one service method, and encodes the result or error. Keep business
ifs out of handlers → [[separation-of-concerns]]. - Make handlers methods on a struct carrying dependencies (
type Handler struct{ svc *Service }). This injects collaborators without globals and keeps handlers testable → [[dependency-injection]]. - Centralise JSON: a
writeJSON(w, code, v)sets the header, writes the status, and encodes; a decode helper caps the body withhttp.MaxBytesReaderand canDisallowUnknownFields()→ [[input-validation]]. - Header/status/body order matters: set headers, then
WriteHeader(code), then write the body. The firstWriteimplicitly sends200, so set the status first → [[response-writer]]. - The service normalises (
TrimSpace,ToLower) before validating, so "Ada@X.com " and "ada@x.com" collide as duplicates. Normalisation is a business decision, not the DB's. - Service methods take
ctx context.Contextfirst and pass it straight to the repo, so a cancelled request stops work all the way down → [[context-first-param]]. - Return domain errors (
%w-wrapped sentinels) from the service; let the handler map them to status. Never return*sql.ErrNoRowspast the repository → [[error-wrapping]].
💻 Code Examples¶
func (h *Handler) create(w http.ResponseWriter, r *http.Request) {
var in struct {
Email string `json:"email"`
}
if err := json.NewDecoder(r.Body).Decode(&in); err != nil {
writeErr(w, http.StatusBadRequest, "bad json")
return
}
u, err := h.svc.Register(r.Context(), in.Email) // all rules live in the service
if err != nil {
writeErr(w, statusFor(err), err.Error()) // one place maps domain err → status
return
}
writeJSON(w, http.StatusCreated, u)
}
Full code:
examples/month-04/layered/main.go· Run:go run ./examples/month-04/layered
🏋️ Exercises / Practice¶
| Exercise | Status | Link |
|---|---|---|
| Service.Register with normalise + validate + conflict | ✅ | exercises/month-04/week-4/service |
| Notes handler (201/400/404/422 + auto-405) | ✅ | exercises/month-04/week-4/httpapi |
🐛 Mistakes Made¶
- Called
w.WriteHeader(201)thenw.Header().Set("Content-Type", ...)— too late; headers must be set beforeWriteHeader. Reordered. - Put the email-uniqueness check in the handler at first; moved it into the service so gRPC/CLI callers get the same rule.
❓ Open Questions¶
- Should validation errors return 400 or 422? Settled on: malformed JSON → 400; well-formed JSON that violates rules → 422 (matches Day 099).
🧠 Active Recall (answer without looking)¶
- Q: What are the only three jobs of an HTTP handler in this design?
A
Decode the request, call exactly one service method, encode the response (or map the error to a status). No business logic.- Q: Why set response headers before calling
WriteHeader?
A
Because `WriteHeader` (and the first `Write`, which calls it implicitly with 200) flushes the status line and headers. Anything set on `w.Header()` afterward is ignored.🪶 Feynman Reflection¶
The handler is a waiter: it takes your order (decodes JSON), passes it to the kitchen (service), and brings back the plate (encodes JSON) — it never cooks. The kitchen owns the recipes (validation, uniqueness, normalisation). If I later open a drive-through (gRPC), I hire a new waiter but the kitchen is unchanged.
🕳️ Knowledge Gaps¶
- Streaming large responses with
json.Encodervs buffering for exactContent-Length— revisit when payloads grow.
✅ Summary¶
I built thin, dependency-injected handlers and a service that owns all rules, communicating through domain types and %w-wrapped sentinel errors.
⏭️ Next Steps / Prep for Tomorrow¶
- Day 108: implement the repository against a real SQL database and write forward-only migrations.
| Time spent | Difficulty | Confidence |
|---|---|---|
| 90 min | 🟦🟦⬜⬜⬜ | 🟦🟦🟦⬜⬜ |
Suggested commit: feat(project): handlers and service layer (day 107)