Generic Web Handlers

Part 3: Build re-usable HTTP handlers

Cheikh seck
10 min readAug 13, 2023
https://unsplash.com/photos/IlxX7xnbRF8Lars Kienle

To paraphrase Bill Kennedy: “consistency wins the game.” The consistency being referred to here is the ability to handle web requests and responses in a consistent manner; this includes consistently processing errors, requests and log messages — the pundits refer to this as Good Code. Bill achieves this by setting guidelines around layers and structuring his code to enable further consistency.

This statement resonates with me because I used to find myself writing a web handler and wondering which error key am I sending with each response; I’d hesitate, guess and look through the code to figure out which message I was using. By doing this, I lose my train of thought. It’s crazy how structuring a handler can reduce this need to constantly check documentation.

In the previous post, we observed database wrapper interface design and some benefits of having one. Let’s take another step forward and design HTTP handlers that will expose the database wrapper to the internet. Prior to writing the handler code, I’ll add two implementations of the database wrapper; these iterations will have user-defined types that resemble API objects you’d find in the wild; one implementation will manage posts and the other comments.

Clever Data Design

Each API object will have a created at and id field; to avoid redefining these fields, I’ll define a base object to embed with each new type. In Go, this approach is known as composition — which is the process of combining smaller objects to build complex ones.

Listing 1

type Object struct {
ID int `json:"id"`
CreatedAt time.Time `json:"created_at"`
}

Listing 1 depicts the base object that will serve as a starting point for future user defined types.

Listing 2

type Post struct {
Object
Text string `json:"text"`
CommentCount int `json:"comment_count"`
Comments []Comment `json:"comments,omitempty"`
}

Listing 2 is the first API object I’ll define; Post represents a social media post, there is a text field, a comment count and an array of comments that belong to a post. If you notice, the first field listed is called Object and this is how composition is achieved; as a result, the fields from Object (ID and CreatedAt) will now exist within type Post. There is one gotcha though; to populate the inherited fields when constructing a struct, you must pass the Object ‘s fields as field name Object. This name will change depending on the name of the user defined type.

Listing 3

Post{
Object: Object{
CreatedAt: time.Unix(
int64(timestamp),
0,
),
ID: id,
},
Text: text,
CommentCount: comments,
}

Listing 3 showcases how a struct leveraging composition can be initialized and how the inherited fields are passed.

Listing 4

type Comment struct {
Object
Comment string `json:"comment"`
PostId int `json:"post_id,omitempty"`
}

Listing 4 depicts a comment. A comment will store the comment’s text and the post id it belongs to.

A Database Wrapper for the Web

Listing 5

type DatabaseWrapper interface {
Create(any) error
Read(int, any) error
ReadAll(int, any) error
Update(int, any) error
Delete(int) error
}

Listing 5 depicts an updated version of the database wrapper interface defined in the previous post; this revision has an extra method (ReadAll) to get all the records.

Listing 6

type PostTable struct {
db *sql.DB
commentTable DatabaseWrapper
}

Listing 6 depicts the user defined type that will implement the database wrapper interface. There is a db field that will be used to query the database and a field named commentTable that represents the wrapper for the comments table. I picked this architecture to preserve the freedom of picking which database provider I can use to store my comments; this is important to mention because I can feel the SQL wizards gearing up to say “why not use a join statement to fetch the comments.”

Listing 7

func NewPostTable(
db *sql.DB,
comments DatabaseWrapper,
) *PostTable {

return &PostTable{
db,
comments,
}
}

Listing 7 depicts the factory function that constructs the PostTable struct. I won’t share the entire implementation except for the Read method; you can also find the entire code base in the link below.

https://github.com/cheikh2shift/miwfy/tree/main/p2

Listing 8

func (db *PostTable) Read(q int, r any) error {

var text string
var timestamp,
comment_count, id int
var comments []Comment

row := db.db.QueryRow("SELECT id,text,comment_count, created_at FROM posts WHERE id = ?", q)
err := row.Scan(&id, &text, &comment_count, &timestamp)

if err != nil {
return err
}

result := Post{
Object: Object{
CreatedAt: time.Unix(
int64(timestamp),
0,
),
ID: id,
},
Text: text,
CommentCount: comment_count,
}

err = db.commentTable.ReadAll(q, &comments)

if err != nil {
return err
}

result.Comments = comments
applyDataToPointer(result, r)

return nil
}

Listing 8 depicts the PostTable's Read method; this snippet showcases how the function leverages the commentTable field to load all the comments of a post when individually queried. This design will add this aspect of volatility to your code and enable you to have the freedom to pick which database you want to store your comments in because you can swap out the SQL implementation of the database wrapper with another one of your choice.

Listing 9

type CommentTable struct {
db *sql.DB
}

func NewCommentTable(db *sql.DB) *CommentTable {
return &CommentTable{
db: db,
}
}

Listing 9 showcases the CommentTable and the factory function used to construct one.

Generic Handler

I’ll be using the GIN framework to build this pseudo social media API. A brute force approach to achieve consistency is by defining generic functions to handle web requests because it’ll be the same underlying code that handles the web requests.

Listing 10

func Add[T any](db DatabaseWrapper) gin.HandlerFunc {
return func(c *gin.Context) {
var req T

err := c.BindJSON(&req)

if err != nil {
c.JSON(
http.StatusBadRequest,
gin.H{
"error": err.Error(),
},
)
return
}

}
}

Listing 10 depicts the initial Add function; this function is generic and the types allowed is constrained to any (or interface{}), in other words, the function will accept any type. The function has one parameter, db , and this will enable me to pass any database wrapper implementation to the handlers. The goal of this handler is to add an entry to the database by invoking the database wrapper’s Create method.

Listing 11

var req T

err := c.BindJSON(&req)

Listing 11 is pulled from the function defined in listing 10. Variable req has type T; this is how I tell the Go compiler that this variable should get it’s type from the one passed on the function’s invocation and will let me take advantage of the JSON bindings of the passed struct type.

Listing 12

func Add[T any](db DatabaseWrapper) gin.HandlerFunc {
return func(c *gin.Context) {
var req T

err := c.BindJSON(&req)

if err != nil {
c.JSON(
http.StatusBadRequest,
gin.H{
"error": err.Error(),
},
)
return
}

err = db.Create(req)

if err != nil {
c.JSON(
http.StatusInternalServerError,
gin.H{
"error": err.Error(),
},
)
return
}

c.JSON(
http.StatusOK,
gin.H{
"result": "Data created",
},
)
}
}

Listing 12 depicts the entire Add function. One implicit requirement is that the type passed on the generic function’s invocation must be supported by the underlying database wrapper.

Listing 13

func Update[T any](db DatabaseWrapper) gin.HandlerFunc {
return func(c *gin.Context) {
var req T

id := c.Param("id")

if id == "" {
c.JSON(
http.StatusBadRequest,
gin.H{
"error": "URL parameter `id` is required",
},
)
return
}

err := c.BindJSON(&req)

if err != nil {
c.JSON(
http.StatusBadRequest,
gin.H{
"error": err.Error(),
},
)
return
}

_, err = strconv.Atoi(id)

if err != nil {
c.JSON(
http.StatusBadRequest,
gin.H{"error": err.Error()},
)
return
}

}
}

Listing 13 depicts the Update function that will be used to update a record in the database. The handler expects a URL parameter named id to be passed; this parameter needs to be a number and the handler performs a pseudo validation by calling the strconv package’s Atoi function.

Listing 14

func Update[T any](db DatabaseWrapper) gin.HandlerFunc {
return func(c *gin.Context) {
var req T

id := c.Param("id")

if id == "" {
c.JSON(
http.StatusBadRequest,
gin.H{
"error": "URL parameter `id` is required",
},
)
return
}

err := c.BindJSON(&req)

if err != nil {
c.JSON(
http.StatusBadRequest,
gin.H{
"error": err.Error(),
},
)
return
}

intId, err := strconv.Atoi(id)

if err != nil {
c.JSON(
http.StatusBadRequest,
gin.H{"error": err.Error()},
)
return
}

err = db.Update(intId, req)

if err != nil {
c.JSON(
http.StatusInternalServerError,
gin.H{
"error": err.Error(),
},
)
return
}

c.JSON(
http.StatusOK,
gin.H{
"result": "Row updated",
},
)
}
}

Listing 14 depicts the entirety of the Update handler; as observed, the function is calling the database wrapper interface’s Update method.

Listing 15

func Read[T any](db DatabaseWrapper) gin.HandlerFunc {
return func(c *gin.Context) {

var r []T

err := db.ReadAll(0, &r)

if err != nil {
c.JSON(
http.StatusInternalServerError,
gin.H{
"error": err.Error(),
},
)
return
}

c.JSON(
http.StatusOK,
gin.H{
"result": r,
},
)

}
}

Listing 15 depicts the Read handler; this handler will be used to list all the records in a database. If you notice, variable r has type []T; this tells the compiler the variable is an array of the type passed with the invocation of the generic function.

Listing 16

func ReadOne[T any](db DatabaseWrapper) gin.HandlerFunc {
return func(c *gin.Context) {

query := c.Param("id")
var r T

idInt, err := strconv.Atoi(query)

if err != nil {
c.JSON(
http.StatusBadRequest,
gin.H{
"error": err.Error(),
},
)
return
}

err = db.Read(idInt, &r)

if err != nil {
c.JSON(
http.StatusNotFound,
gin.H{
"error": err.Error(),
},
)
return
}

c.JSON(
http.StatusOK,
gin.H{
"result": r,
},
)

}
}

Listing 16 depicts the function to read one record from the database; unlike the function in listing 15 variable r has type T.

Listing 17

func Remove[T any](db DatabaseWrapper) gin.HandlerFunc {
return func(c *gin.Context) {

id := c.Param("id")

if id == "" {
c.JSON(
http.StatusBadRequest,
gin.H{
"error": "URL parameter `id` is required",
},
)
return
}

idInt, err := strconv.Atoi(id)

if err != nil {
c.JSON(
http.StatusBadRequest,
gin.H{"error": err.Error()},
)
return
}

err = db.Delete(idInt)

if err != nil {
c.JSON(
http.StatusInternalServerError,
gin.H{
"error": err.Error(),
},
)
return
}

c.JSON(
http.StatusOK,
gin.H{
"result": "row removed",
},
)
}
}

Listing 17 depicts the delete handler; this function will call the database wrapper’s delete method to remove a record from the database. In my previous post, I described a handler that returned an interface and perform type switching to determine how to handle a request; in this post, I’m achieving consistency by reusing the same methods to mutate and read from the database. The former solution is better, however I’m managing to achieve consistency by re-using the same handlers for each request type.

Putting It Together

With the handlers in place, it’s time to move onto the implementation.

Listing 18

func main() {

// Database setup complete
// sqlite test. init database in memory
sqldb, err := sql.Open("sqlite3", "./test.db")

if err != nil {
log.Fatal(err)
}

// close connection
// after function returns.
defer sqldb.Close()

if _, err := sqldb.Exec(`
DROP TABLE IF EXISTS posts;
DROP TABLE IF EXISTS comments;
CREATE TABLE posts(id INTEGER PRIMARY KEY, text TEXT,comment_count INT, created_at INT);
CREATE TABLE comments(id INTEGER PRIMARY KEY,post_id INTEGER,text TEXT,created_at INT);`); err != nil {
panic(err)
}
...
}

Listing 18 depicts the code I’ll use to initialize the database connection and add the tables required for the API to operate. I’m adding two tables, one for posts and the other for comments.

Listing 19

func main(){
...
commentsDB := NewCommentTable(sqldb)
postTable := NewPostTable(sqldb, commentsDB)

r := gin.Default()
...
}

In listing 19, I’m constructing the comments and posts database wrapper; the idea is that each table will have its own database wrapper.

Listing 20

func main(){
...
posts := r.Group("/posts")
{
posts.POST( "",
Add[Post](postTable),
)

posts.DELETE(
"/:id",
Read[Post](postTable),
)

posts.PUT(
"/:id",
Update[Post](postTable),
)

posts.GET(
"/:id",
ReadOne[Post](postTable),
)

posts.GET(
"",
Read[Post](postTable),
)
}
...
}

In listing 20, I’m assigning the different routes to the appropriate handler function.

Listing 21

...

comments := posts.Group("/comment")
{
comments.POST(
"",
Add[Comment](commentsDB),
)
}
...

Listing 21 showcases how the Add function can be re-used to add a new comment to the database; as long as the underlying database wrapper supports the passed type, the code will work.

Listing 22

...
comments.PUT(
"/:id",
Update[Comment](commentsDB),
)
...

Listing 22 is another example of re-using a handler and passing a different type.

To test this code, I put together a little bash script that will add a post, list all posts, comment on a post and then read the post with all of its comments. Here is the contents of this script:

curl -X POST http://localhost:3000/posts \
-H 'Content-Type: application/json' \
-d '{"text" : "hello world" }' | json_pp

echo "\n"

curl http://localhost:3000/posts | json_pp

echo "\n"

curl -X POST http://localhost:3000/posts/comment \
-H 'Content-Type: application/json' \
-d '{"comment" : "hello world" , "post_id" : 1 }' | json_pp

echo "\n"

curl http://localhost:3000/posts/1 | json_pp

Listing 23

$ sh cmds.sh 
% Total % Received % Xferd Average Speed Time Time Time Current
Dload Upload Total Spent Left Speed
100 50 100 25 100 25 3436 3436 --:--:-- --:--:-- --:--:-- 8333
{
"result" : "Data created"
}


% Total % Received % Xferd Average Speed Time Time Time Current
Dload Upload Total Spent Left Speed
100 264 100 264 0 0 14184 0 --:--:-- --:--:-- --:--:-- 14666
{
"result" : [
{
"comment_count" : 1,
"created_at" : "2023-08-13T15:46:51Z",
"id" : 1,
"text" : "hello world"
},
]
}


% Total % Received % Xferd Average Speed Time Time Time Current
Dload Upload Total Spent Left Speed
100 69 100 25 100 44 1525 2685 --:--:-- --:--:-- --:--:-- 4600
{
"result" : "Data created"
}


% Total % Received % Xferd Average Speed Time Time Time Current
Dload Upload Total Spent Left Speed
100 314 100 314 0 0 30703 0 --:--:-- --:--:-- --:--:-- 34888
{
"result" : {
"comment_count" : 1,
"comments" : [
{
"comment" : "hello world",
"created_at" : "2023-08-13T15:46:52Z",
"id" : 1
},
],
"created_at" : "2023-08-13T15:46:51Z",
"id" : 1,
"text" : "hello world"
}
}

The output of the bash script is showcased in listing 23. And again, it’s the same handler that is used for both API object types; the same function to add a post is used to add a comment.

Conclusion

Pairing generics with interfaces makes for flexible and re-usable code. I didn’t want dive into too much detail of the database wrapper interface because this article is about building handlers for a web API. You can find the entire code to test with here:

https://github.com/cheikh2shift/miwfy/tree/main/p2

In the next post, I’d like to go a layer up and add authentication to the server; as expected, my approach will consist of defining an interface with the methods I’ll need to authorize a user.

--

--