From Source to Service
A real HTTP service
Lesson 1 of 5
What you'll learn
- Stand up an HTTP server with
net/httpand 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.
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.
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.