Jon Calhoun
Posted on May 6, 2018
This article was original posted on my website at calhoun.io
NOTE: Most of the code and ideas in this post are things I have been experimenting with. That doesn't mean the ideas and lessons aren't valuable, but it does mean that you shouldn't just blindly follow this pattern. It has its own set of pros and cons that should be considered on a case-by-case basis. That said, the pattern has been working very well for me and using a design to isolate parsing data from application logic is a critical step in building web applications that support multiple formats (HTML and JSON API), as we will explore in a future post.
We've all likely seen a web application in Go with a handler function that looks something like this:
type WidgetHandler struct {
DB *sql.DB
// Renders the HTML page w/ a form to create a widget
CreateTemplate *template.Template
// Renders a list of widgets in an HTML page
ShowTemplate *template.Template
}
func (handler *WidgetHandler) Create(w http.ResponseWriter, r *http.Request) {
// Most HTML based web apps will use cookies for sessions
cookie, err := r.Cookie("remember_token")
if err != nil {
http.Redirect(w, r, "/login", http.StatusFound)
return
}
// Hash the value since we store remember token hashes in our db
rememberHash := hash(cookie.Value)
// Then look up the user in the database by hashed their remember token
var user User
row := handler.DB.QueryRow(`SELECT id, email FROM users WHERE remember_hash=$1`, rememberHash)
err = row.Scan(&user.ID, &user.Email)
if err != nil {
http.Redirect(w, r, "/login", http.StatusFound)
return
}
// From here on we can assume we have a user and move on to processing
// the request
var widget Widget
widget.UserID = user.ID
err = r.ParseForm()
// TODO: handle the error
widget.Name = r.FormValue("name")
// postgres specific SQL
const insertWidgetSql = `
INSERT INTO widgets (user_id, name)
VALUES ($1, $2)
RETURNING id`
err = handler.DB.QueryRow(insertWidgetSql, widget.UserID, widget.Name).Scan(&widget.ID)
if err != nil {
// Render the error to the user and the create page
w.Header().Set("Content-Type", "text/html")
handler.CreateTemplate.Execute(w, map[string]interface{}{
"Error": "Failed to create the widget...",
"Widget": widget,
})
return
}
// Redirect the user to the widget
http.Redirect(w, r, fmt.Sprintf("/widgets/%d", widget.ID), http.StatusFound)
}
The exact details may vary - for instance the application may use another database, it might create UserService
and WidgetService
interfaces instead of directly writing SQL, and you might use a framework/router like echo, but generally speaking the code will look roughly the same. The first few lines of a handler will be used to parse data, then we will go about doing whatever we really wanted to do, then finally we will render any results or errors.
Handlers are a data parsing and rendering layer
If we look back at our original code, it is shocking just how much of that code is actually just parsing and rendering. The entire cookie retrieval section is used just to get a remember token or redirect the user if there is an error. Once we have the token we perform a database lookup, but again this is quickly followed with a error handling and rendering logic. Then comes parsing the form, getting the widget name, and rendering any errors that occur while creating the widget. Finally, we are able to redirect the user to the new widget if it is created, but if you think about it the redirect is basically just rendering logic as well.
All in all, about 60% of our code is just parsing data and rendering results/errors.
Parsing this data isn't intrinsically bad, but what I do find disturbing is the fact that before data is parsed, requirements are unclear. Think about it - if I handed you this function definition and asked you to test it, could you tell me what data it expected?
func (handler *WidgetHandler) Create(w http.ResponseWriter, r *http.Request)
You might be able to infer from the WidgetHandler
type and the function name - Create
- that this is used to create a widget, so we need some information describing a widget, but would you know what format that data should be in? Would you know that the user needs to be signed in via a cookie based session?
Even worse, we can't even infer which parts of the WidgetHandler
need to be instantiated for this to work. If we scan the code we can clearly see that we use the DB
field, and it looks like we render the CreateTemplate
when there is an error so we need to set that, but we had to look through all of the code to see what all was used.
NOTE: In this example which fields we use are obvious, but imagine our WidgetHandler
was used to create, update, publish, and perform many other actions on a widget. In that case our WidgetHandler
type would have a lot more fields and we surely wouldn't need them all to be set to test just this handler.
Handler functions need to be vague; there really isn't a viable way to create an http server without having a vague definition of what an incoming HTTP request looks like and then writing some code to parse that incoming data. Even if we created reusable middleware and leveraged the to store the parsed data, we still need to write and test those middleware, and it doesn't solve the problem of having unclear data requirements for our handler functions. So how do we fix that problem?
The service object pattern
Rather than fighting the fact that we need to parse data in our handlers, I have found that what works better is to embrace it and make those handlers strictly data parsing rendering layers. That is, in my http handlers I try to avoid any logic that isn't related to parsing or rendering data and instead embrace a pattern very similar to the service objects pattern in Ruby.
NOTE: In reality, I even try to pull as much data rendering out of the handlers as possible too. See for more ideas on how to do this.
The way the pattern works is pretty simple - rather than writing logic in my handlers to do things like create a widget, I instead pull that ocde out into a function that has clear data requirements and is easy to test. For instance, in the widget creation example I might create something like this:
func CreateWidget(db *sql.DB, userID int, name string) error {
var widget Widget
widget.Name = name
widget.UserID = userID
const insertWidgetSql = `
INSERT INTO widgets (user_id, name)
VALUES ($1, $2)
RETURNING id`
err = db.QueryRow(insertWidgetSql, widget.UserID, widget.Name).Scan(&widget.ID)
if err != nil {
return err
}
return nil
}
Now it is much clearer that in order to create a widget, we need to have a database connection, the ID of the user creating the widget, and the name of the widget.
NOTE: You don't have to create such specific requirements here. For instance, I'll often create functions like this that expect both a User
and a Widget
as its arguments instead of the more specific userID
and name
arguments. That choice is up to you to make.
A more interesting example
This particular example is pretty boring, so let's look at a more interesting example. Let's imagine we wanted to handle having a user sign up for our application, and when this happens we create the user in our database, send the user a welcome email, and add them to our mailing list tool. A traditional handler might look something like this:
func (handler *UserHandler) Signup(w http.ResponseWriter, r *http.Reqeust) {
// 1. parse user data
r.ParseForm()
email = r.FormValue("email")
password = r.FormValue("password")
// 2. hash the pw and create the user, handling any errors
hashedPw, err := handler.Hasher.Bcrypt(password)
if err != nil {
// ... handle this
}
var userID int
err := handler.DB.QueryRow("INSERT INTO users .... RETURNING id", email, hashedPw).Scan(&userID)
if err != nil {
handler.SignupForm.Execute(...)
return
}
// 3. Add the user to our mailing list
err = handler.MailingService.Subscribe(email)
if err != nil {
// handle the error somehow
}
// 4. Send them a welcome email
err = handler.Emailer.WelcomeUser(email)
if err != nil {
// handle the error
}
// 5. Finally redirect the user to their dashboard
http.Redirect(...)
}
As you can see, we have a good bit of error handling, and in each of those if
blocks we could easily need to render an error page, send the user back to the signup page, or anything else. We also end up using quite a few pieces of the handler - the MailingService
, SignupForm
, Emailer
, and the Hasher
- and none of these are obvious for testing purposes.
What makes this even worse is that testing each of these individual pieces is somewhat annoying. If we just wanted to verify that calling this endpoint created a user in the database we would still need to at least stub out all of those other pieces.
In cases like this, splitting our code into a few service objects that have clear requirements and can be independently tested is incredibly useful.
type UserCreator struct {
DB *sql.DB
Hasher
Emailer
MailingService
}
func (uc *UserCreator) Run(email, password string) (*User, error) {
pwHash, err := uc.Hasher.BCrypt(password)
if err != nil {
return nil, err
}
user := User{
Email: email,
}
row := uc.DB.QueryRow("INSERT INTO users .... RETURNING id", email, hashedPw)
err = row.Scan(&user.ID)
if err != nil {
return nil, err
}
err = uc.MailingService.Subscribe(email)
if err != nil {
// log the error
}
err = uc.Emailer.WelcomeUser(email)
if err != nil {
// log the error
}
return &user, nil
}
Now we can easily test the code used to create a user; the dependencies are clear and we don't need to mess around with HTTP requests. It is just regular old Go code.
We also have the added benefit of simplifying our handler code. It no longer needs to mess around dealing with non-fatal errors that just need information logged, and we can instead focus on just parsing data.
type UserHandler struct {
signup func(email, password string) (*User, error)
}
func (handler *UserHandler) Signup(w http.ResponseWriter, r *http.Reqeust) {
// 1. parse user data
r.ParseForm()
email = r.FormValue("email")
password = r.FormValue("password")
user, err := handler.signup(email, password)
if err != nil {
// render an error
}
http.Redirect(...)
}
To instantiate this code, we would write something like:
uc := &UserCreator{...}
uh := &UserHandler{signup: uc.Run}
And then we would be free to use the methods on uh
as http.HandlerFunc
s in our router.
More, but clearer code
This approach clearly requires more code. We now need to setup a UserCreator
type and then set its Run
function to the signup
field in the UserHandler
, but by doing this we have clearly separated the role of each function and made it much easier to to test our code. We no longer need to even have a database connection to test our handler, and could instead test it with code like this:
uh := &UserHandler{
signup: func(email, password) (*User, error) {
return &User{
ID: 123,
Email: email,
}, nil
}
}
Similarly, when testing our UserCreator
we don't need to use the httptest
package at all. Neato! 🙌
Finally, as we will see in a followup post (I'm working on it still - it's long), this also opens the door for writing applications that are mostly agnostic of their input/output formats. That is, we could take an existing web application and add JSON API support with fairly minimal effort.
Did you enjoy this article? Join my mailing list!
If you enjoyed this article, please consider joining my mailing list.
I will send you roughly one email every week letting you know about new articles (like this one) or screencasts (ex) that I am working on or have published recently. No spam. No selling your emails. Nothing shady - I'll treat your inbox like it was my own.
As a special thank you for joining, I'll also send you both screencast and ebook samples from my course, Web Development with Go.
Posted on May 6, 2018
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.
Related
November 29, 2024