Create a Handler

You'll learn
  • How to write and test your first HTTP handler.
  • What a health endpoint is, and how it works with load balancing.

Okay, now we have a Server that doesn’t do anything. Time to change that.

We’re now going to add our first handler, and that’s going to be a so-called health endpoint. It has a very important reason to exist: it is the endpoint that the load balancer calls to check that your app is still healthy and can receive traffic.

Confused? Let me explain a bit further.

Load balancing

The load balancer is the first machine in the cloud that your user’s requests hit when they call your web app from the browser. It typically handles your TLS certificates, and then forwards the incoming request to an instance of your app. It’s possible that you only have a single instance running, so that instance will receive all traffic from the load balancer. But this mechanism is what enables scaling up your app to multiple instances. You would do this to handle a lot of traffic, and to make sure that you can serve requests even when the machine your container runs on goes down.

And this last bit is exactly what the health endpoint is for: if the load balancer doesn’t receive a successful response from it when it checks regularly, no traffic is forwarded to that instance anymore. So if you make sure that you have at least one instance running, you will always be able to serve your sweet HTTP responses to your users.

Get me one of those health endpoints

So what does the endpoint look like? It’s just a regular old HTTP handler that responds with a HTTP 200 OK. Paste the code below into handlers/health.go, and I’ll explain after. Alternatively, checkout the finished code with:

$ git fetch && git checkout --track golangdk/health

See the diff on Github.

handlers/health.go
package handlers import ( "net/http" "github.com/go-chi/chi/v5" ) func Health(mux chi.Router) { mux.Get("/health", func(w http.ResponseWriter, r *http.Request) { // 🤪 }) }

The first thing you’ll note is that we’re in the handlers package. This is where all our handlers will live.

All top-level functions in that package follow the same pattern. They are passed the (remember the mux from the Server construction?) as well as any other dependencies they’ll need, like a database connector. The health endpoint doesn’t currently have any dependencies, so it just gets the mux.

You might wonder why the handler gets the mux passed to register the route itself. I’m glad you wondered! Often you’ll see code where something like the setupRoutes function has all route path definitions, and the handlers don’t know about their own routes. The reason the handlers register themselves is twofold.

First, it makes testing easier. You don’t have to repeat the route in the test, or maybe not test the route at all. Instead, you just pass a real mux in the test, and create your requests against that. We’ll see how that works soon.

Second, it has the route definition closer to the implementation, and this often makes sense. Your URL path often contains variables, like IDs of what page your user is requesting, and the name of that ID is closely tied to the implementation. You have to use the same name to get the value, so it’s nice to be able to see the name close to the code.

What else does the handler do? Absolutely nothing! We don’t have any dependencies to check right now. We don’t even have to return the status code, because handlers default to returning HTTP 200 OK if nothing else is set.

But right now, if you would call /health on your server, it would return HTTP 404 Not Found. That’s because we haven’t registered the handler in our routes function yet. Let’s do that in server/routes.go:

server/routes.go
package server import ( "canvas/handlers" ) func (s *Server) setupRoutes() { handlers.Health(s.mux) }

Voila, calling /health should now return successfully. How do we know? Tests!

Testing health

Let’s see how we test the handler. Put this into handlers/health_test.go:

handlers/health_test.go
package handlers_test import ( "io" "net/http" "net/http/httptest" "testing" "github.com/go-chi/chi/v5" "github.com/matryer/is" "canvas/handlers" ) func TestHealth(t *testing.T) { t.Run("returns 200", func(t *testing.T) { is := is.New(t) mux := chi.NewMux() handlers.Health(mux) code, _, _ := makeGetRequest(mux, "/health") is.Equal(http.StatusOK, code) }) } // makeGetRequest and returns the status code, response headers, and the body. func makeGetRequest(handler http.Handler, target string) (int, http.Header, string) { req := httptest.NewRequest(http.MethodGet, target, nil) res := httptest.NewRecorder() handler.ServeHTTP(res, req) result := res.Result() bodyBytes, err := io.ReadAll(result.Body) if err != nil { panic(err) } return result.StatusCode, result.Header, string(bodyBytes) }

We’re using the httptest package from the standard library in a small helper function. This is to save the response from the handler, so we can inspect it. We pass a real Chi mux to the handler, and check that it responds with a http.StatusOK. And that’s pretty much it for now.

Next, we're finally adding the missing parts to be able to start up our app outside of tests.

Review questions

Sign up or log in to get review questions with teacher feedback by email! 📧

Questions?

Get help at support@golang.dk.