typestar

serve_json.go in Go

A JSON API with the standard mux: routes, middleware, graceful shutdown.

// Command serve_json runs a small JSON API over the standard library.
package main

import (
    "context"
    "encoding/json"
    "errors"
    "fmt"
    "log/slog"
    "net/http"
    "sync"
    "time"
)

type tour struct {
    Slug  string `json:"slug"`
    Title string `json:"title"`
    Steps int    `json:"steps"`
}

type store struct {
    mu    sync.RWMutex
    tours map[string]tour
}

func (s *store) get(slug string) (tour, bool) {
    s.mu.RLock()
    defer s.mu.RUnlock()
    found, ok := s.tours[slug]
    return found, ok
}

func writeJSON(w http.ResponseWriter, status int, body any) {
    w.Header().Set("Content-Type", "application/json")
    w.WriteHeader(status)
    _ = json.NewEncoder(w).Encode(body)
}

func routes(s *store) http.Handler {
    mux := http.NewServeMux()

    mux.HandleFunc("GET /healthz", func(w http.ResponseWriter,
        r *http.Request) {
        writeJSON(w, http.StatusOK, map[string]string{"status": "ok"})
    })

    mux.HandleFunc("GET /tours/{slug}", func(w http.ResponseWriter,
        r *http.Request) {
        found, ok := s.get(r.PathValue("slug"))
        if !ok {
            writeJSON(w, http.StatusNotFound,
                map[string]string{"error": "no such tour"})
            return
        }
        writeJSON(w, http.StatusOK, found)
    })

    return withLogging(mux)
}

func withLogging(next http.Handler) http.Handler {
    return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        started := time.Now()
        next.ServeHTTP(w, r)
        slog.Info("request", "method", r.Method, "path", r.URL.Path,
            "took", time.Since(started).Round(time.Microsecond))
    })
}

func main() {
    data := &store{tours: map[string]tour{
        "basics": {Slug: "basics", Title: "Language basics", Steps: 30},
    }}

    server := &http.Server{
        Addr:              "127.0.0.1:8099",
        Handler:           routes(data),
        ReadHeaderTimeout: 5 * time.Second,
    }

    go func() {
        slog.Info("listening", "addr", server.Addr)
        if err := server.ListenAndServe(); err != nil &&
            !errors.Is(err, http.ErrServerClosed) {
            slog.Error("serve", "err", err)
        }
    }()

    time.Sleep(50 * time.Millisecond) // stand-in for a signal
    shutdownCtx, cancel := context.WithTimeout(context.Background(),
        5*time.Second)
    defer cancel()
    if err := server.Shutdown(shutdownCtx); err != nil {
        fmt.Println("forced shutdown:", err)
    }
    slog.Info("stopped cleanly")
}

How it works

  1. The 1.22 mux handles the method and the path variable.
  2. Middleware wraps the whole mux, so every route is logged.
  3. Shutdown waits for in-flight requests, with its own deadline.

Keywords and builtins used here

The run, in numbers

Lines
97
Characters to type
2140
Tokens
543
Three-star pace
115 tpm

At the three-star pace of 115 tokens a minute, this run takes about 283 seconds.

Type this snippet

Step 1 of 1 in Encore, step 26 of 26 in Standard library & project layout.

← Previous