Custom Go HTTP handlers using generics

2021-12-19
Go

Few days ago Go 1.18beta1 was released and with it the first official generics support. I was embarrassed with standard Go’s HTTP handler functions for quite a while. If you are not familiar with Go, you probably wonder why, but if you are familiar, I believe you know. For example, implementing a RESTful API using idiomatic Go requires lot of code repetition in order to JSON decode request bodies and JSON encode response bodies. This problem was possible to solve in some way but never in such elegant way like using generics.

First, let’s take a look how Go’s idiomatic HTTP handler functions look like: func(res http.ResponseWriter, req *http.Request)

res is just an interface providing several methods to set response and req is a concrete implementation of Go’s HTTP request. In order to get path or query parameters, you have to play around with req, but let’s forget now about that. Even worse is that in your RESTful API you don’t get concrete decoded JSON request body as concrete type, but instead you have to parse it every time. Then at the end of the request you have do encode your data to JSON response body again. So, lot’s of encoding and decoding which can’t be hidden in an easy way, at least not without generics. It’s not the point to hide these steps, but to focus to business logic and not to irrelevant tasks like this. When I say hide it’s not about magic, but about having a framework which does this for you so you can focus to the business login. If you write lot of API’s in Go, you probably realize this without my explanation. It’s so boring to handle JSON encoding/decoding and possible errors. Let’s focus to the real stuff.

In order to overcome this, I made gap today, small package which provides a wrapper over custom gap’s HTTP handler functions providing idiomatic HTTP functions at the end. So, it can be used with Go’s standard library HTTP implementation. gap’s handler functions accept custom request with concrete type and also custom response with concrete data type. Here is the signature: func(*gap.Request[I]) *gap.Response[O]. Generic type I here is the type of the JSON request body and O is the type of the data returned within the gap.Response. Besides the data, this custom response contains also HTTP status code and errors that may have ocurred. So, your custom handler would become concrete request type as input and you may return concrete response type as output. And the render function of gap would take care to send the JSON response to the client.

But, let’s jump into coding, then it will be more clear what is this about. Here is a gap’s handler example:

type hello struct {
	Message string `json:"message"`
}

helloHandler := func(req *gap.Request[struct{}]) *gap.Response[hello] {
    return &gap.Response[hello]{
        Data: &hello{
            Message: "Hello world!",
        },
    }
}

Because our helloHandler is GET endpoint, our concrete request body will be just struct{}, so no body. However, our response will contain data of hello type. What does it mean? Here is the gap.Response type:

type Response[T any] struct {
	StatusCode int      `json:"-"`
	Data       *T       `json:"data,omitempty"`
	Errors     []string `json:"errors,omitempty"`
}

So, *gap.Response[hello] in the snippet above means that we are returning *gap.Response type with data field of type hello, which is private type in this case. In order to use our helloHandler as standard Go’s HTTP handler, it’s enough just to wrap it using gap.Wrap. Besides the custom HTTP handler function, gap.Wrap requires a logger with lax.Logger interface, but this is not relevant to this article, let’s focus on the API. Here is how wrapping looks like:

log, _ := lax.NewDefaultZapAdapter("json", "debug")
http.HandleFunc("/hello", gap.Wrap(helloHandler, log))

Our handler function will be triggered on /hello path, will receive no body (struct{}) and will send gap.Response with gap.Response.Data of type hello (concrete private type in this case, but can be exported as well). Both JSON request body decoding and JSON response body encoding will be done by gap package.

For further documentation how to use this package, please check this test and this example test . At the end I would like to mention that this is project is in a very early stage and the API may suffer lot of modifications in the future. So, gap should not be used in production until the first release, but it can show the power of Go’s generics and give you ideas how to improve Go’s RESTful API’s further. Feel free to contribute to the project.

Running stack of microservices using docker-compose and acim/go-reflex image

How to easily run multiple Go microservices locally using just one image
Go microservice docker-compose docker

Upcoming Go's generics

Find out more about upcoming generics in Go
Go Golang generics

Go vanity imports

Release your packages under custom domain using vanity imports
Go Golang