Day 45 — net/http server
Day 45 — net/http server
Stage V · ~3h
Goal: Build a small JSON HTTP API with Go 1.22+ ServeMux method/path patterns, a middleware chain, and clean handler structure—stdlib only.
Why this day exists
net/http is the default way Go speaks to the world. Frameworks come and go; Handler stays. After Day 44’s sockets, today you operate one layer up:
http.Handler/HandlerFunc
- Routing with modern
ServeMux
- Middleware as
func(http.Handler) http.Handler
- JSON request/response discipline
Theory 1 — The Handler interface
type Handler interface {
ServeHTTP(ResponseWriter, *Request)
}
type HandlerFunc func(ResponseWriter, *Request)
func (f HandlerFunc) ServeHTTP(w ResponseWriter, r *Request) {
f(w, r)
}Everything is a Handler: muxes, middleware, your API.
ResponseWriter basics
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusOK) // first write of status; once only effectively
_, _ = w.Write(body)Headers must be set before WriteHeader / first Write.
Theory 2 — ServeMux patterns (Go 1.22+)
Modern patterns support methods and wildcards:
mux := http.NewServeMux()
mux.HandleFunc("GET /healthz", health)
mux.HandleFunc("GET /items/{id}", getItem)
mux.HandleFunc("POST /items", createItem)
mux.HandleFunc("GET /files/{path...}", getFile) // trailing wildcardExtract path values:
id := r.PathValue("id")Matching notes
- More specific patterns win over less specific.
"GET /x"does not matchPOST /x.
- Host-specific patterns exist (
"GET example.com/x")—rarely needed in labs.
- Legacy patterns without methods still work; prefer method-aware forms for APIs.
If your local Go is older than 1.22, upgrade to the volume baseline 1.26.x. Method patterns are required for today’s labs.
Theory 3 — Middleware chain
type Middleware func(http.Handler) http.Handler
func Chain(h http.Handler, mws ...Middleware) http.Handler {
for i := len(mws) - 1; i >= 0; i-- {
h = mws[i](h)
}
return h
}
func RequestID(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
id := r.Header.Get("X-Request-ID")
if id == "" {
id = fmt.Sprintf("%d", time.Now().UnixNano())
}
w.Header().Set("X-Request-ID", id)
next.ServeHTTP(w, r)
})
}
func Recover(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
defer func() {
if rec := recover(); rec != nil {
http.Error(w, "internal error", http.StatusInternalServerError)
}
}()
next.ServeHTTP(w, r)
})
}Order matters: recover outermost, then logging, then auth (later days).
Theory 4 — Server struct
srv := &http.Server{
Addr: ":8080",
Handler: handler,
ReadHeaderTimeout: 5 * time.Second, // mitigate slowloris
ReadTimeout: 10 * time.Second,
WriteTimeout: 20 * time.Second,
IdleTimeout: 60 * time.Second,
}
err := srv.ListenAndServe()Always set ReadHeaderTimeout at minimum in production-shaped code.
Graceful shutdown (deepened on Day 50):
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
defer cancel()
_ = srv.Shutdown(ctx)Theory 5 — JSON helpers
func writeJSON(w http.ResponseWriter, status int, v any) {
w.Header().Set("Content-Type", "application/json; charset=utf-8")
w.WriteHeader(status)
_ = json.NewEncoder(w).Encode(v)
}
func readJSON(r *http.Request, dst any) error {
defer r.Body.Close()
dec := json.NewDecoder(io.LimitReader(r.Body, 1<<20))
dec.DisallowUnknownFields()
if err := dec.Decode(dst); err != nil {
return err
}
// optional: reject trailing junk
if err := dec.Decode(&struct{}{}); err != io.EOF {
return fmt.Errorf("trailing data")
}
return nil
}Worked example — items API (in-memory)
package api
import (
"net/http"
"sync"
)
type Item struct {
ID string `json:"id"`
Name string `json:"name"`
}
type Server struct {
mu sync.RWMutex
items map[string]Item
mux *http.ServeMux
}
func New() *Server {
s := &Server{items: make(map[string]Item), mux: http.NewServeMux()}
s.routes()
return s
}
func (s *Server) Handler() http.Handler {
return Chain(s.mux, Recover, RequestID, AccessLog)
}
func (s *Server) routes() {
s.mux.HandleFunc("GET /healthz", s.health)
s.mux.HandleFunc("GET /items", s.listItems)
s.mux.HandleFunc("GET /items/{id}", s.getItem)
s.mux.HandleFunc("POST /items", s.createItem)
}
func (s *Server) health(w http.ResponseWriter, r *http.Request) {
writeJSON(w, http.StatusOK, map[string]string{"status": "ok"})
}
func (s *Server) getItem(w http.ResponseWriter, r *http.Request) {
id := r.PathValue("id")
s.mu.RLock()
item, ok := s.items[id]
s.mu.RUnlock()
if !ok {
writeJSON(w, http.StatusNotFound, map[string]string{"error": "not found"})
return
}
writeJSON(w, http.StatusOK, item)
}
// listItems, createItem: similar; generate ID with crypto/rand or strconvImplement AccessLog middleware logging method, path, status (needs a wrapping ResponseWriter that records status code—classic pattern).
type statusWriter struct {
http.ResponseWriter
code int
}
func (w *statusWriter) WriteHeader(code int) {
w.code = code
w.ResponseWriter.WriteHeader(code)
}Lab 1 — Scaffold
mkdir -p ~/lab/90daysofx/01-go/day45
cd ~/lab/90daysofx/01-go/day45
go mod init example.com/day45Packages: api/ handlers, cmd/itemsd/main.go server process.
Lab 2 — Routes + middleware
Requirements:
GET /healthz→{"status":"ok"}
POST /itemsJSON body{"name":"..."}→201+ item with id
GET /items/{id}→ item or 404
GET /items→ array
- Middleware: request ID + recover + basic log
Manual smoke:
go run ./cmd/itemsd
curl -s localhost:8080/healthz
curl -s -X POST localhost:8080/items -d '{"name":"widget"}' -H 'Content-Type: application/json'Lab 3 — Validation
Reject empty name with 400 and JSON error body. Limit body size. Unknown JSON fields → 400.
Lab 4 — Method mismatch
POST /healthz should 405 (ServeMux behavior with method patterns). Confirm with curl; note status.
Lab 5 — Wrap for later tests
Export New().Handler() so Day 49 can use httptest without listening. Keep state behind mutex for concurrent requests.
Common gotchas
| Gotcha | Fix |
|---|---|
| WriteHeader after Write | Set status first path carefully |
| Forgetting Content-Type | Set on JSON responses |
| Shared map without mutex | sync.RWMutex |
| Middleware order inverted | Apply in reverse in Chain |
| No ReadHeaderTimeout | Set on http.Server |
Using old mux only /items/ slash tricks |
Prefer 1.22 patterns + PathValue |
| Returning plaintext errors inconsistently | Always JSON error objects for API |
Checkpoint
- Method-aware routes work
- PathValue extracts
{id}
- Middleware chain applied
- JSON create/get/list
- Body limit + basic validation
- Server timeouts set
Commit
git add .
git commit -m "day45: http server ServeMux 1.22+ JSON API middleware"Write three personal gotchas before continuing.
Tomorrow
Day 46 — net/http client: timeouts at every layer, transports, context cancellation, and safe body handling.