← Docs
Recipe

Recipe: Go net/http handler scaffold

A production-ready HTTP handler skeleton with structured logging, request ID propagation, timeout middleware, and graceful shutdown.

package main

import (
  "context"
  "log/slog"
  "net/http"
  "os"
  "time"
)

func main() {
  h := slog.NewJSONHandler(os.Stderr, nil)
  l := slog.New(h)

  mux := http.NewServeMux()
  mux.HandleFunc("GET /health", health(l))

  srv := &http.Server{
    Addr:         ":8080",
    Handler:      requestID(timeout(30*time.Second, mux)),
    ReadTimeout:  5 * time.Second,
    WriteTimeout: 10 * time.Second,
    IdleTimeout:  120 * time.Second,
  }

  go func() {
    l.Info("listening", "addr", srv.Addr)
    if err := srv.ListenAndServe(); err != nil {
      l.Error("server error", "err", err)
    }
  }()

  // graceful shutdown omitted for brevity
  select {}
}

Middleware chain

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 = "gen-" + time.Now().Format("20060102150405")
    }
    w.Header().Set("X-Request-ID", id)
    ctx := context.WithValue(r.Context(), "rid", id)
    next.ServeHTTP(w, r.WithContext(ctx))
  })
}

func timeout(d time.Duration, next http.Handler) http.Handler {
  return http.TimeoutHandler(next, d, "request timed out")
}

Handler pattern

func health(l *slog.Logger) http.HandlerFunc {
  return func(w http.ResponseWriter, r *http.Request) {
    l.InfoContext(r.Context(), "health check")
    w.Header().Set("Content-Type", "application/json")
    w.WriteHeader(http.StatusOK)
    w.Write([]byte(`{"status":"ok"}`))
  }
}

This scaffold uses only the standard library. For production, pair with the Meridian loader to enforce license checks before the handler serves traffic.