How To Create Golang REST API: Project Layout Configuration [Part 3]

Written by danstenger | Published 2020/04/22
Tech Story Tags: golang | rest-api | middleware | go | coding | programming | api | backend

TLDR How To Create Golang REST API: Project Layout Configuration [Part 3] The project is about 3 simple parts, no nasty additives. The idea of a middleware in context of route handlers is that we construct our handlers from multiple, small functions - middlewares. Middlewares allow easy code re-usability and handlers become much easier to reason about. I'll continue working on users resource from previous posts by adding additional logic to it. Next I plan to accept user id as URL and make sure it's a valid numeric string. If client sends malformed id, I'm skipping it for demonstration purposes.via the TL;DR App

Good cake is the one you can easily slice into parts with no crumbs falling apart. That's all this project is about: 3 simple parts, no nasty additives. In part 1 and part 2 I've explained the basics of setting up golang project using docker, creating configurable server, interacting with DB, adding routes and handlers.
What happens when multiple handlers have to reuse same logic?
What is the most elegant way of solving this issue?
This is where the middleware comes into play and will be my main focus here.
The idea of a middleware in context of route handlers is that we construct our handlers from multiple, small functions - middlewares. That allows easy code re-usability and handlers become much easier to reason about.
I'll continue working on users resource from previous posts by adding additional logic to it. One common thing each handler might have is request logging. Before implementing it, there's a small utility function that will help me construct handlers:
// pkg/middleware/middleware.go 

package middleware

import (
	"github.com/julienschmidt/httprouter"
)

type Middleware func(httprouter.Handle) httprouter.Handle

func Chain(f httprouter.Handle, m ...Middleware) httprouter.Handle {
	if len(m) == 0 {
		return f
	}

	return m[0](Chain(f, m[1:]...))
}
The
Chain
function will accept any number of functions of a Middleware type and call them one by one giving us full control of the flow, more on that later. Now that I have this in place, I'll add simple request logging middleware. Since it's going to be reused in multiple handlers, I'll put it in pkg/middleware directory. All handler specific middleware will live next to handler itself:
// pkg/middleware/logging.go

package middleware

import (
	"net/http"

	"github.com/boilerplate/pkg/logger"
	"github.com/julienschmidt/httprouter"
)

func LogRequest(next httprouter.Handle) httprouter.Handle {
	return func(w http.ResponseWriter, r *http.Request, p httprouter.Params) {
		logger.Info.Printf("%s - %s %s %s", r.RemoteAddr, r.Proto, r.Method, r.URL.RequestURI())
		next(w, r, p)
	}
}
Simple higher order function that accepts and returns function of a
httprouter.Handle
type and reads some data from request in between. Next I plan to accept user id as url parameter and I want to make sure it's a valid numeric string. If it's not, I'll simply respond to client with status code 412. Sounds like this logic can be implemented as a middleware:
// cmd/api/handlers/getuser/validarequest.go
 
package getuser

import (
	"context"
	"fmt"
	"net/http"
	"strconv"

	"github.com/boilerplate/cmd/api/models"
	"github.com/julienschmidt/httprouter"
)

func validateRequest(next httprouter.Handle) httprouter.Handle {
	return func(w http.ResponseWriter, r *http.Request, p httprouter.Params) {
		uid := p.ByName("id")

		id, err := strconv.Atoi(uid)
		if err != nil {
			w.WriteHeader(http.StatusPreconditionFailed)
			fmt.Fprintf(w, "malformed id")
			return
		}

		ctx := context.WithValue(r.Context(), models.CtxKey("userid"), id)
		r = r.WithContext(ctx)
		next(w, r, p)
	}
}
Same principle as in logging middleware. Context is a standard way of sharing data between middleware and I added user id to it for demonstration purposes. If client sends malformed id, I'm skipping all following steps in my middleware chain by simply not calling the
next
function and responding to client directly from here.
Next step is
getuser.Do
handler itself:
// cmd/api/handlers/getuser/getuser.go

package getuser

import (
	"database/sql"
	"encoding/json"
	"errors"
	"fmt"
	"net/http"

	"github.com/boilerplate/cmd/api/models"
	"github.com/boilerplate/pkg/application"
	"github.com/boilerplate/pkg/middleware"
	"github.com/julienschmidt/httprouter"
)

func getUser(app *application.Application) httprouter.Handle {
	return func(w http.ResponseWriter, r *http.Request, p httprouter.Params) {
		defer r.Body.Close()

		id := r.Context().Value(models.CtxKey("userid"))
		user := &models.User{ID: id.(int)}

		if err := user.GetByID(r.Context(), app); err != nil {
			if errors.Is(err, sql.ErrNoRows) {
				w.WriteHeader(http.StatusPreconditionFailed)
				fmt.Fprintf(w, "user does not exist")
				return
			}

			w.WriteHeader(http.StatusInternalServerError)
			fmt.Fprintf(w, "Oops")
			return
		}

		w.Header().Set("Content-Type", "application/json")
		response, _ := json.Marshal(user)
		w.Write(response)
	}
}

func Do(app *application.Application) httprouter.Handle {
	mdw := []middleware.Middleware{
		middleware.LogRequest,
		validateRequest,
	}

	return middleware.Chain(getUser(app), mdw...)
}
I first create a slice of all required middleware which later becomes a variadic function parameter.
middleware.Chain
is implemented in a way that it calls functions right to left so first LogRequest is going to be called followed by validateRequest and getUser.
Few things to point out here. Notice that middleware in mdw slice has slightly different signature than getUser. This is because getUser is our last step in the chain and it will not be calling
next
function to jump to next steps of a chain. Second thing is how I read value from context. Let's start the service before testing the handler:
docker-compose up --build
Now let's make a request with foobar as user id. Since foobar is not numeric string, I'd expect malformed id message in response:
Now, let's try valid id. Currently I have no record with id
2
in users table so would expect user does not exist message:
You can find whole project here. I really hope this will be a good starting point for you and help building new, exciting things.
Stay safe, eat cake and keep coding!

Written by danstenger | GO and functional programming enthusiast
Published by HackerNoon on 2020/04/22