Skip to content

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

📝 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 with http.MaxBytesReader and can DisallowUnknownFields() → [[input-validation]].
  • Header/status/body order matters: set headers, then WriteHeader(code), then write the body. The first Write implicitly sends 200, 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.Context first 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.ErrNoRows past 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) then w.Header().Set("Content-Type", ...) — too late; headers must be set before WriteHeader. 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)

  1. 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.
  1. 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.Encoder vs buffering for exact Content-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)