BuildBot

From Source to Service

A real HTTP service

Lesson 1 of 5

What you'll learn

  • Stand up an HTTP server with net/http and the Go 1.22+ ServeMux
  • Decode and encode JSON request/response bodies
  • Wire dependencies into handlers as closures instead of globals

In Node you reach for Express or Fastify before writing a single route. In Go you don't: net/http is the production server. It handles connection pooling, timeouts, TLS, and HTTP/2, and since Go 1.22 its router — http.ServeMux — matches on method and path patterns, including wildcards. For most services there is no framework to install.

func main() {
    mux := http.NewServeMux()
    mux.HandleFunc("GET /notes/{id}", getNote)
    mux.HandleFunc("POST /notes", createNote)

    slog.Info("listening", "addr", ":8080")
    log.Fatal(http.ListenAndServe(":8080", mux))
}

"GET /notes/{id}" only matches GET requests to that shape of path; a POST to the same URL gets an automatic 405 Method Not Allowed. Inside the handler, r.PathValue("id") reads the wildcard — the Go 1.22 equivalent of Express's req.params.id.

JSON has no res.json() sugar, but the pattern is two lines each way:

func createNote(w http.ResponseWriter, r *http.Request) {
    var in struct{ Title string `json:"title"` }
    if err := json.NewDecoder(r.Body).Decode(&in); err != nil {
        http.Error(w, "bad json", http.StatusBadRequest)
        return
    }
    w.Header().Set("Content-Type", "application/json")
    json.NewEncoder(w).Encode(map[string]string{"title": in.Title})
}

Dependencies via closures

Handlers need a database, a logger, config. Idiomatic Go passes them in by making the handler a function that returns a handler — a closure over its dependencies, exactly like a curried Express middleware factory:

func handleGetNote(store *NoteStore) http.HandlerFunc {
    return func(w http.ResponseWriter, r *http.Request) {
        note, err := store.Get(r.PathValue("id"))
        if err != nil {
            http.Error(w, "not found", http.StatusNotFound)
            return
        }
        json.NewEncoder(w).Encode(note)
    }
}

// wiring: mux.HandleFunc("GET /notes/{id}", handleGetNote(store))

No globals, no dependency-injection framework — the wiring is plain function calls in main, which makes it trivially testable (lesson 4).

Use log/slog, not fmt.Println

Go 1.21 added log/slog, structured logging in the standard library. slog.Info("created", "id", id) emits key-value pairs you can ship as JSON. Reach for it from day one; retrofitting logs is miserable.

The challenge is a JS model of the 1.22 mux: patterns route by method + path, wildcards become path values, and handlers close over a store.

Method+path routing (JS model)

Run it. This models Go 1.22's ServeMux: routes match method AND path, {id} captures a path value, and the handler is a closure over the store. Note the 405 when the method doesn't match.

Loading editor…
Knowledge check

With mux.HandleFunc("GET /notes/{id}", h) on Go 1.22+, what happens to a POST request to /notes/7?

Next: one main.go doesn't scale — Go's module system, cmd/ and internal/, and the tooling that keeps a repo honest.

Saved on this device. Sign in to sync your progress everywhere.