Over a million developers have joined DZone.
{{announcement.body}}
{{announcement.title}}

Creating a Web App Using the Go Language Gorilla Web Toolkit

DZone's Guide to

Creating a Web App Using the Go Language Gorilla Web Toolkit

Read on to learn how to use the Go language and it's Gorilla tool kit, but making a simple web application with REST API.

· Web Dev Zone
Free Resource

Add user login and MFA to your next project in minutes. Create a free Okta developer account, drop in one of our SDKs to your application and get back to building.

Go is a great language for web applications. The Go core library has excellent HTTP support, and the language's support for asynchronous execution lends itself well to high-performance web applications.

While you could start a web app from scratch with the core net/http package, you can save yourself some time and pain by using one of the many web app toolkits available for Go. This article will cover a simple web app using the Gorilla web toolkit'smux package, with Postgres as the backend database. To talk to Postgres from Go, we'll use lib/pq with the database/sql core package.

All of the packages used in the example application are shipped with our ActiveGo beta, so you can download ActiveGo and run this application on your own system.

This article assumes you already have a basic understanding of the Go language and tools, so I'll focus on the design of the web app and the use of mux and other libraries.

I'm going to build a simple web app with a REST API that takes a chunk of text and gives you the SHA256 hash (in hex form) for that text. It will also go the other way, from SHA256 to text. This is going to be a huge money-maker for us, so the app does user authorization and billing. To keep this post shorter, I'll omit the user registration and purchasing parts of the application.

All of the code and tools for this application are in a public GitHub repo.

Let's start by taking a look at our database schema:

CREATE TABLE "user" (
    user_id  CHAR(64)   PRIMARY KEY, -- a SHA256 token for web requests
    name     TEXT       NOT NULL,
    credit   BIGINT     DEFAULT 0 -- credits in cents
);

CREATE TABLE hash_text (
    hash     CHAR(64)   PRIMARY KEY,
    text     TEXT
);

Now that we have a very sophisticated schema, let's define our REST API. We need endpoints for turning a text into a hash, one to return the text for a hash, and for good measure we'll add an endpoint to look up a user by their user_id:

  • GET /user/me - returns a JSON object containing all of the information we have about the user that calls these endpoints.
  • POST /text - accepts a JSON object with a single key, text. We'll hash this text and return a JSON object with a single key, hash. If the requester is out of credit, this returns a 402 (Payment Required).
  • GET /text/{hash} - returns a JSON object containing the text for a hash. If no such hash exists it returns a 404 (Not Found).

All endpoints will require that the user supplies a X-HashText-User-ID containing a valid user_id. Any request without this token will receive a 401 (Unauthorized) response.

So how does this work with Gorilla's mux? We'll use mux to set up routes to handle each of these endpoints, as well as to look up the hash in the /text/{hash} URI. We set these endpoints in the router.go file:

package main

import "github.com/gorilla/mux"

func makeRouter() *mux.Router {
    r := mux.NewRouter()
    r.HandleFunc("/user/me", wrapHandler(userHandler)).Methods("GET")
    r.HandleFunc("/text", wrapHandler(textHandler)).Methods("POST")
    r.HandleFunc("/text/{hash}", wrapHandler(textHashHandler)).Methods("GET")
    return r
}

In the code above, we've only matched based on paths and the HTTP methods, but the mux.Router type supports many other options.

So we have some routes, but to what are we routing? The HandleFunc method expects its second argument to be a function with the signature (http.ResponseWriter, *http.Request). We're taking advantage of Go's excellent support for first-class functions by calling wrapHandler() to wrap all of my handlers with code to check user authorization. Here's what wrapHandler() looks like in the handlers.go file:

func wrapHandler(
handler func(w http.ResponseWriter, r *http.Request),
) func(w http.ResponseWriter, r *http.Request) {

h := func(w http.ResponseWriter, r *http.Request) {
if !userIsAuthorized(r) {
w.WriteHeader(http.StatusUnauthorized)
return
}
handler(w, r)
}
return h
}

This takes any handler function and wraps it with another function. That wrapper function calls userIsAuthorized and returns a 401 (Unauthorized) response if the user is not authorized. Otherwise, it calls the original handler. All that userIsAuthorized does is check that the user ID in the X-HashText-User-ID header exists in the database:

func userIsAuthorized(r *http.Request) bool {
userID := r.Header.Get("X-HashText-User-ID")
if userID == "" {
return false
}

var found bool
err := db.QueryRow(`SELECT 1 FROM "user" WHERE user_id = $1`, userID).Scan(&found)
switch {
case err == sql.ErrNoRows:
return false
case err != nil:
log.Printf("Query to look up user failed: %v", err)
return false
}

return found
}

We call r.Header.Get() to get the value of the header. If it's not set, or the header exists but is empty, this value will be an empty string. If that's the case we can skip the database lookup.

For the database lookup, we call db.QueryRow. This method is used to execute a query that is expected to return exactly zero or one row. We then call Scan() on the returned *db.Row to get the actual value. If no rows were found, the Scan() method will return a sql.ErrNoRows error. If any other sort of error is returned, that indicates a more serious problem. If this were a real production application, we'd want a robust logging system, but for now, we just use the logpackage to spit out an error message.

Here's what our UserHandler itself looks like:

func userHandler(w http.ResponseWriter, r *http.Request) {
userID := r.Header.Get("X-HashText-User-ID")

row := db.QueryRow(`SELECT name, credit FROM "user" WHERE user_id = $1`, userID)

var name string
var credit int
err := row.Scan(&name, &credit)
switch {
case err == sql.ErrNoRows:
w.WriteHeader(http.StatusNotFound)
return
case err != nil:
log.Printf("Query to look up user failed: %v", err)
w.WriteHeader(http.StatusInternalServerError)
return
}

sendJSONResponse(w, userDocument{UserID: userID, Name: name, Credit: credit})
}

Once again, we use db.QueryRow() to look up a row in the database, this time to get the user's name credit amount. If we find a matching user, we send a JSON document as the response. Since all of our handlers can return a JSON document, we've abstracted that into its own function.

The pattern for sending a response with the http.ResponseWriter interface always follows the same pattern:

  1. Set the response header.
  2. Call WriteHeader(status) passing the HTTP status for the response.
  3. Write the response body using Write() if there is a response body. Note that the Write()method takes an argument of the type []byte, not a string. You can useio.WriteString() if you have a string to send.

Here's what sendJSONResponse looks like:

func sendJSONResponse(w http.ResponseWriter, data interface{}) {
body, err := json.Marshal(data)
if err != nil {
log.Printf("Failed to encode a JSON response: %v", err)
w.WriteHeader(http.StatusInternalServerError)
return
}

w.Header().Set("Content-Type", "application/json; charset=UTF-8")
w.WriteHeader(http.StatusOK)
_, err = w.Write(body)
if err != nil {
log.Printf("Failed to write the response body: %v", err)
return
}
}

First, we try to encode our response as JSON using json.Marshal(). If encoding fails for some reason, we log an error and return a 500.

We then call w.Header().Set() to set the Content-Type header in the response. Finally, we call w.Write() to write the response body. Since the json.Marshal() method function returns bytes, not a string, we don't need to use io.WriteString. If the call to Write() fails, we log an error, but it's too late to change the HTTP status.

The other handlers are mostly similar, but let's take a look at how we handle a request with a body and request for a path with a variable. The textHandler looks at the request body:

func textHandler(w http.ResponseWriter, r *http.Request) {
userID := r.Header.Get("X-HashText-User-ID")
if !userHasCredit(userID) {
sendErrorMessage(w, "You are out of credit. Please pay us more money.", http.StatusPaymentRequired)
return
}

body, err := ioutil.ReadAll(r.Body)
if err != nil {
log.Printf("Failed to read the request body: %v", err)
w.WriteHeader(http.StatusInternalServerError)
return
}

var td textDocument
if err := json.Unmarshal(body, &td); err != nil {
sendErrorMessage(w, "Could not decode the request body as JSON", http.StatusBadRequest)
return
}

// This will work with an empty string, for some value of work. If we
// wanted to make this a bit smarter, we'd check the length of the text
// submitted and return an error if it's empty.
//
// In a production application we might want to do the insert in a
// goroutine, but this makes testing much more complicated.
hash := sha256String(td.Text)
insertText(td.Text, hash, userID)
sendJSONResponse(w, hashDocument{Hash: hash})
}

We first call io.ReadAll(r.Body) to get the request body's content. The http.Request type'sBody field implements the io.ReadCloser interface, which means that the ReadAll function can read from it directly. We then use the json.Unmarshal function to convert the request body's JSON content into a struct. If this fails, we send an error message with a 400 (Bad Request) status. The error message itself is just plain text.

The textHashHandler function looks at the path for a variable:

func textHashHandler(w http.ResponseWriter, r *http.Request) {
vars := mux.Vars(r)
row := db.QueryRow(`SELECT text FROM hash_text WHERE hash = $1`, vars["hash"])

var text string
err := row.Scan(&text)
switch {
case err == sql.ErrNoRows:
w.WriteHeader(http.StatusNotFound)
return
case err != nil:
log.Printf("Query to look up text by hash failed: %v", err)
w.WriteHeader(http.StatusInternalServerError)
return
}

sendJSONResponse(w, textDocument{Text: text})
}

We told mux to look for a variable in this path when we created our router:

 r.HandleFunc("/text/{hash}", wrapHandler(textHashHandler)).Methods("GET")

This tells mux that any path matching /text/... matches this handler and that the portion after /text/ is a variable named hash. Note that this only matches one level of the path, so a path like /text/.../more/stuff would not match. We can look up the matched variables by callingmux.Vars(r). This returns a map[string]string with all the matched variables. We look up the hash key in this map and attempt to look that hash up in the database. If the hash exists we return the text in a JSON document, otherwise we return a 404 (Not Found).

It looks like we haven't used mux for much in this example, but under the hood, it's doing a lot of work for us. If we had to implement the routing ourselves, along with matching on HTTP methods and looking up variables in the path, that would require dozens of lines of code. And we've only scratched the surface of what the mux router can do for us!

Future Improvements

There are many things we could do to improve this code, including:

  • Implement proper error logging to syslog.
  • Create proper models instead of doing (repetitive) SQL queries in the handler code.
  • Make the database handle injectable to remove some gross code in our tests.
  • Use easyjson for improved JSON performance.
  • Return errors in JSON form and do a better job of returning the right error codes.
  • Adding more endpoints, for example, to look up all hashes created by a user, to add credit, etc.

Launch your application faster with Okta’s user management API. Register today for the free forever developer edition!

Topics:
web dev ,go ,libraries ,rest api

Published at DZone with permission of Dave Rolsky, DZone MVB. See the original article here.

Opinions expressed by DZone contributors are their own.

{{ parent.title || parent.header.title}}

{{ parent.tldr }}

{{ parent.urlSource.name }}