The Golden HTTP handler function signature.

Consistent Request Handling With Gin

Cheikh seck
3 min readJul 27, 2023

Go is one of the best languages to build a web service with and I make this claim based on experience. The problem is that the language is easy to use at a surface level and this aspect of simplicity makes it very easy to misuse. In this post, I’m going to share with you my ideal way of writing HTTP handler code with Gin; this is a culmination of writing code with Gin for a year now as well as nuggets of knowledge picked up from Bill Kennedy’s Ultimate Service class with Kubernetes.

Abstracting API response management

Go leverages static typing, so each time I see code that decodes JSON into a map I die a little inside; because one of the benefits of static typing is the ability to create a contract that your code will abide by.

If you’re not familiar with Gin, it’s a framework that enables you to map code (gin.HanlderFunc) to endpoints. By default, Gin expects you to determine how you want to handle a response within the handler; however, this approach offers too much freedom and undermines this idea of consistent request handling.

Listing 1

type Handler func(c *gin.Context) any

type Error struct {
Code int
Error string
}

func myAdapter(h Handler) gin.HandlerFunc {

return func(c *gin.Context) {

res := h(c)

switch v := res.(type) {
case gin.H:
// here v has type gin.H
c.JSON(
http.StatusOK,
v,
)
case Error:
// here v has type Error
c.JSON(
v.Code,
gin.H{"error": v.Error},
)
default:
c.JSON(
// this response code is not accurate
// but I felt it would be cool
// to return this error code as a means
// to communicate a supported response
// was not returned by the server.
http.StatusExpectationFailed,
gin.H{
"error": "Unknown handler response",
},
)
}

}

}

In listing 1, I’m defining a type called Handler that is similar to the gin.HandlerFunc signature except for the fact that it returns a type of any. I’m also defining a custom struct type named Error that has a code and text field that is read by the myAdapter function to generate and send an error response.

The myAdapter function is performing a type-switch; this will enable me to return an Error or a response from a handler function and have Go determine which type was returned to accurately write the response. Type gin.H is a fancy way to return map[string]interface{} and will add extra flexibility with the responses I can provide from HTTP Handlers.

Listing 2


func ErrorHandler(c *gin.Context) any {

return Error{
http.StatusInternalServerError,
"Sample error",
}
}

Listing 2 is a sample handler that will return an error. Without all the prep work, this handler will look like this:

Listing 3

func ErrorHandler(c *gin.Context){

c.JSON(
http.StatusInternalServerError,
gin.H{
"error": "Sample error",
}
}

Listing 3 can be considered the vanilla approach to writing an error response with Gin; the problem with this design is that for each error you need to provide you’ll need to write out the JSON object that is sent out. Given sometime, it’s easy to lose consistency because the JSON key error must be provided with each response. It’s also important to note that changing the JSON key for the error message will be extremely difficult to perform.

Listing 4

func HelloHandler(c *gin.Context) any {

return gin.H{
"data": "Hello World",
}
}

Listing 4 is an HTTP handler that will write {“data": “Hello World”} as a response with status code 200.

Listing 5

func main() {

r := gin.Default()

r.GET("/test", myAdapter(HelloHandler))
r.GET("/test_error", myAdapter(ErrorHandler))

err := r.Run()

if err != nil {
panic(err)
}
}

Listing 5 depicts the implementation of the functions and demonstrates how a custom function signature can be adapted for use with Gin.

Conclusion

The key take-away from this post is to demonstrate how you can supply an API response by returning data from a function; with this approach, you can ensure consistent error and response handling.

Now, there are other methods to abstract handler response processing, but requiring a handler to return data will ensure that any function you define is providing a response to the end user of your API.

You can find the code for this post here:

--

--