- 2019/09/14

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, and event.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.