TLDR: After writing several backend web services in Go, which is mostly REST API server, the most comfortable way for me to write HTTP request handler are using explicit dependency per endpoint.
I want the code that is explicitly defined. Predictable. No magic. Boring.
Let say I have POST /v1/auth/login
endpoint (login endpoint). Require valid JSON body contain credential data (email and password).
That submitted data, for example, will then verified against stored values, do some other event publish and then return a response contain a valid user token if matched, otherwise return an error.
Go net/http
has a very simple HTTP handler signature: http.HandlerFunc
, which is only func(ResponseWriter, *Request)
where ResponseWriter
is http.ResponseWriter
interface, and *Request
is http.Request
pointer.
For those login endpoint, I want to set explicitly requirement for that endpoint in a single file per endpoint, so I have following content:
1. Required request structure and its validation
// login.go
package auth
type LoginRequest struct {
Email string `json:"email"`
Password string `json:"password"`
}
func (req *LoginRequest) Validate() error {
// do your validation here
}
2. Response structure
// login.go
package auth
type LoginResponse struct {
Token string `json:"token"`
ExpiredAt string `json:"expired_at"`
}
3. Request handler
// login.go
package auth
func Login(
credential repo.UserCredential,
token service.Token,
eventPub event.Publisher, /*, ...*/
) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
req := new(LoginRequest)
if err := json.NewDecoder(r.Body).Decode(req); err != nil {
// process and write error here
return
}
if err := req.Validate(); err != nil {
// process and write error here
return
}
// user req, handle login process, error, etc....
// ...
// write LoginResponse{} here
return
}
}
Please note that
repo.UserCredential
,service.Token
, andevent.Publisher
, they are interface.
Then it can be mounted to any router/mux compatible with net/http
, for example, here I use Chi:
// main.go
// setup credentialRepo, tokenService, eventPublisher
r := chi.NewRouter()
r.Post("/v1/auth/login", auth.Login(credentialRepo, tokenService, eventPublisher))
That’s it, from the request handler definition, I know exactly what are the requirements, how the request validated and how the response filled.
And also, for best practice and testability, I use interface for request handler constructor param(s) and avoid “magic call” inside the handler.