Home > OS >  Efficient way to make REST handlers in Go (without repeating code)?
Efficient way to make REST handlers in Go (without repeating code)?

Time:05-11

Currently I have too much repeated code for the handlers:

type GuestMux struct {
  http.ServeMux
}

func main() {
    guestMux := NewGuestMux()
    http.ListenAndServe(":3001", guestMux)
}

func NewGuestMux() *GuestMux {
    var guestMux = &GuestMux{}
    guestMux.HandleFunc("/guest/createguest", createGuestHandler)
    guestMux.HandleFunc("/guest/updateguest", updateGuestHandler)
    guestMux.HandleFunc("/guest/getguest", getGuestHandler)

    return guestMux
}

func createGuestHandler(w http.ResponseWriter, r *http.Request) {
  var createGuestReq CreateGuestRequest
  reqBody, err := ioutil.ReadAll(r.Body)
  if err != nil {
    log.Println(err)
    w.WriteHeader(http.StatusInternalServerError)
    return
  }
  err = json.Unmarshal(reqBody, &createGuestReq)
  if err != nil {
    log.Println(err)
    w.WriteHeader(http.StatusBadRequest)
    return
  }
  resp, err := CreateGuest(&createGuestReq)
  if err != nil {
    log.Println(err)
    w.WriteHeader(http.StatusInternalServerError)
    return
  }
  w.Header().Set("Content-Type", "application/json")
  json.NewEncoder(w).Encode(resp)
}

func updateGuestHandler(w http.ResponseWriter, r *http.Request) {
  var updateGuestReq UpdateGuestRequest
  reqBody, err := ioutil.ReadAll(r.Body)
  if err != nil {
    log.Println(err)
    w.WriteHeader(http.StatusInternalServerError)
    return
  }
  err = json.Unmarshal(reqBody, &updateGuestReq)
  if err != nil {
    log.Println(err)
    w.WriteHeader(http.StatusBadRequest)
    return
  }
  resp, err := UpdateGuest(&updateGuestReq)
  if err != nil {
    log.Println(err)
    w.WriteHeader(http.StatusInternalServerError)
    return
  }
  w.Header().Set("Content-Type", "application/json")
  json.NewEncoder(w).Encode(resp)
}

func getGuestHandler(w http.ResponseWriter, r *http.Request) {
  // almost the same as above two handlers, just different method to call and 
    // its parameter type
    ...
}

Is there any nicer way to write the handlers createGuestHandler, updateGuestHandler and getGuestHandler instead of repeating similar code blocks three times. I guess I can use interface but am not sure how to write that. I have about 20 handlers so the repeating code does not seem really maintainable.

//stackoverflow does not allow question with too much code over details so... details here, details there, even more details...//

CodePudding user response:

You can move the common logic to a separate function, and pass everything to it that is specific in each handler.

Let's assume you have these types and functions:

type CreateGuestRequest struct{}
type UpdateGuestRequest struct{}
type CreateGuestResponse struct{}
type UpdateGuestResponse struct{}

func CreateGuest(v *CreateGuestRequest) (resp *CreateGuestResponse, err error) {
    return nil, nil
}

func UpdateGuest(v *UpdateGuestRequest) (resp *UpdateGuestResponse, err error) {
    return nil, nil
}

With generics allowed

If generics are allowed, you can factor all code out of handlers:

func handle[Req any, Resp any](w http.ResponseWriter, r *http.Request, logicFunc func(dst Req) (Resp, error)) {
    var dst Req
    if err := json.NewDecoder(r.Body).Decode(dst); err != nil {
        log.Printf("Decoding body failed: %v", err)
        w.WriteHeader(http.StatusInternalServerError)
        return
    }
    resp, err := logicFunc(dst)
    if err != nil {
        log.Println(err)
        w.WriteHeader(http.StatusInternalServerError)
        return
    }
    w.Header().Set("Content-Type", "application/json")
    if err := json.NewEncoder(w).Encode(resp); err != nil {
        log.Printf("Encoding response failed: %v", err)
    }
}

func createGuestHandler(w http.ResponseWriter, r *http.Request) {
    handle(w, r, CreateGuest)
}

func updateGuestHandler(w http.ResponseWriter, r *http.Request) {
    handle(w, r, UpdateGuest)
}

As you can see, all handler implementations are just a single line! We can even get rid of the handler functions now, as we can create a handler from a logic function (like CreateGuest(), UpdateGuest()).

This is how it would look like:

func createHandler[Req any, Resp any](logicFunc func(dst Req) (Resp, error)) http.HandlerFunc {
    return func(w http.ResponseWriter, r *http.Request) {
        var dst Req
        if err := json.NewDecoder(r.Body).Decode(dst); err != nil {
            log.Printf("Decoding body failed: %v", err)
            w.WriteHeader(http.StatusInternalServerError)
            return
        }
        resp, err := logicFunc(dst)
        if err != nil {
            log.Println(err)
            w.WriteHeader(http.StatusInternalServerError)
            return
        }
        w.Header().Set("Content-Type", "application/json")
        if err := json.NewEncoder(w).Encode(resp); err != nil {
            log.Printf("Encoding response failed: %v", err)
        }
    }
}

And using it:

func NewGuestMux() *GuestMux {
    var guestMux = &GuestMux{}
    guestMux.HandleFunc("/guest/createguest", createHandler(CreateGuest))
    guestMux.HandleFunc("/guest/updateguest", createHandler(UpdateGuest))

    return guestMux
}

Without generics

This solution does not use generics (and works with old Go versions too).

func handle(w http.ResponseWriter, r *http.Request, dst interface{}, logicFunc func() (interface{}, error)) {
    if err := json.NewDecoder(r.Body).Decode(dst); err != nil {
        log.Printf("Decoding body failed: %v", err)
        w.WriteHeader(http.StatusInternalServerError)
        return
    }
    resp, err := logicFunc()
    if err != nil {
        log.Println(err)
        w.WriteHeader(http.StatusInternalServerError)
        return
    }
    w.Header().Set("Content-Type", "application/json")
    if err := json.NewEncoder(w).Encode(resp); err != nil {
        log.Printf("Encoding response failed: %v", err)
    }
}

func createGuestHandler(w http.ResponseWriter, r *http.Request) {
    var createGuestReq CreateGuestRequest
    handle(w, r, &createGuestReq, func() (interface{}, error) {
        return CreateGuest(&createGuestReq)
    })
}

func updateGuestHandler(w http.ResponseWriter, r *http.Request) {
    var updateGuestReq UpdateGuestRequest
    handle(w, r, &updateGuestReq, func() (interface{}, error) {
        return UpdateGuest(&updateGuestReq)
    })
}

CodePudding user response:

There are many ways to avoid repetition here, for example, you could use a decorator pattern, where you can define how to decode/encode and other steps that doesn't include your business logic.

You can check two interesting approaches: One is from Mat: https://pace.dev/blog/2018/05/09/how-I-write-http-services-after-eight-years.html

The other one is the go-kit package (you can check it out on github), but I recommend you to checkout the idea on how to compose decorators instead of installing the library, could be an overkill for your implematation.

CodePudding user response:

Typically REST APIs have just /guest endpoint with single handler that decides what to do based on HTTP method:

  • POST to create
  • GET to retrieve
  • PUT to update the entire record
  • PATCH to update certain fields

You can look at r.Method inside your handler and decide what code to run based on that.

If you are bound to interface shown in your question you can e.g. wrap handler to an anonymous function with expected interface and make it accept an additional argument to decide what to do like this:

guestMux.HandleFunc("/guest/createguest", func(w http.ResponseWriter, r *http.Request) {
      guestHandler(r, w, CREATE)
})
guestMux.HandleFunc("/guest/updateguest", func(w http.ResponseWriter, r *http.Request) {
      guestHandler(r, w, UPDATE)
})
...

(where CREATE and UPDATE are some sort of flags that tell guestHandler() what it should do)

CodePudding user response:

I suggest to have a look to go-kit. It's mainly designed to create services using Hexagonal architecture. It brings a lot of utility functions to avoid repeated code and focus on the business logic.

It has a lot of functionality that may not need but since it's a toolkit (and not a complete framework) you're free to use only the parts that you need.

Examples are also easy to follow.

CodePudding user response:

I have these utility functions : decodeJsonBody, respondJson that I use to simplify response, without adding too much complexity. I wrap it in the Response struct for sending client side error details.

type Response struct {
    Data   interface{} `json:"data"`
    Errors interface{} `json:"errors"`
}

func respondJson(w http.ResponseWriter, data interface{}, err error) {
    w.Header().Set("Content-Type", "application/json")
    if err != nil {
        w.WriteHeader(http.StatusBadRequest)
        err = json.NewEncoder(w).Encode(Response{
            Errors: err.Error(),
        })
        return
    }
    err = json.NewEncoder(w).Encode(Response{
        Data: data,
    })
    if err != nil {
        w.WriteHeader(http.StatusInternalServerError)
        log.Printf("http handler failed to convert response to json %s\n", err)
    }
}

func decodeJsonBody(r *http.Request, v interface{}) error {
    decoder := json.NewDecoder(r.Body)
    return decoder.Decode(v)
}

func updateGuestHandler(w http.ResponseWriter, r *http.Request) {
    var updateGuestReq UpdateGuestRequest
    err := decodeJsonBody(r, &updeateGuestReq)
    if err != nil {
        respondJson(w, nil, err)
        return
    }
    data, err := UpdateGuest(&updateGuestReq)
    respondJson(w, data, err)

}
  • Related