DMarlon Studio

Lesson 19 · 19 of 20

HTTP handlers and httptest · Go

Write an HTTP handler in Go with net/http that answers in JSON, and test it with httptest, no network or server. This is how the Web (Go) target works.

Go · HTTP handlers and httptest in DMarlon Studio: the example's code and its output in the terminal

A web server in Go is, above all, a handler: a function with the signature func(w http.ResponseWriter, r *http.Request). r carries the request (method, path, parameters) and the response is written to w. In DMarlon Studio, Go is the server of the Web (Go) target: the app's pages ask handlers like this one for their data.

priceHandler reads the parameter with r.URL.Query().Get("sku") and looks it up in prices. The switch picks the answer: if the method is not GET, `http.Error` writes status 405 and a message; if the product does not exist, 404. When no status is set, the response goes out as 200.

In the good case, the headers are set first with w.Header().Set("Content-Type", "application/json") (once the body is written they can no longer change), and then json.NewEncoder(w).Encode(...) writes the JSON straight into the response. Since it is a map, the keys come out in alphabetical order: price before sku.

`http.HandlerFunc(priceHandler)` turns the function into an http.Handler, the interface all of net/http works with. HandlerFunc is a function type with a ServeHTTP method that calls the function itself: interfaces and function values working together.

Testing it needs no network. `httptest.NewRequest` builds a request and `httptest.NewRecorder` is a ResponseWriter that keeps whatever is written: rec.Code, the headers and rec.Body. To serve it for real, http.ListenAndServe(":8080", handler) would be enough.

Code · main.go

package mainimport (	"encoding/json"	"fmt"	"net/http"	"net/http/httptest")func priceHandler(w http.ResponseWriter, r *http.Request) { // GET /api/price?sku=...	prices := map[string]float64{"KB-01": 49.9, "MS-02": 19.5}	sku := r.URL.Query().Get("sku")	price, ok := prices[sku]	switch {	case r.Method != http.MethodGet:		http.Error(w, "method not allowed", http.StatusMethodNotAllowed)	case !ok:		http.Error(w, "unknown sku", http.StatusNotFound)	default:		w.Header().Set("Content-Type", "application/json")		json.NewEncoder(w).Encode(map[string]any{"sku": sku, "price": price})	}}func main() {	handler := http.HandlerFunc(priceHandler) // the adapter that makes it an http.Handler	requests := [][2]string{{"GET", "/api/price?sku=KB-01"}, {"GET", "/api/price?sku=ZZ-09"},		{"POST", "/api/price?sku=KB-01"}, {"GET", "/api/price?sku=MS-02"}}	for _, req := range requests {		rec := httptest.NewRecorder() // records status, headers and body: no network		handler.ServeHTTP(rec, httptest.NewRequest(req[0], req[1], nil))		fmt.Printf("%-4s %-21s -> %d %s", req[0], req[1], rec.Code, rec.Body)	}}

Output · go run main.go

GET  /api/price?sku=KB-01  -> 200 {"price":49.9,"sku":"KB-01"}
GET  /api/price?sku=ZZ-09  -> 404 unknown sku
POST /api/price?sku=KB-01  -> 405 method not allowed
GET  /api/price?sku=MS-02  -> 200 {"price":19.5,"sku":"MS-02"}

Key points

  • A handler takes an http.ResponseWriter and an *http.Request.
  • http.Error writes an error status and a message.
  • Set the headers before writing the body.
  • http.HandlerFunc turns a function into an http.Handler.
  • httptest tests handlers with no network or server.

Exercise

Make a request with no sku (/api/price) answer 400 with the text missing sku using http.StatusBadRequest, and add it to requests to check it.