Mat Ryer
·
9 May 2018
Mat Ryer
·
9 May 2018
The way I have written services has changed over time, so I wanted to share how I write the services today—in case the patterns are useful to you and your work.
A Server struct is an object that represents the service, and holds all of its dependencies.
All of my components have a single server structure that usually ends up looking something like this:
type server struct {
db *someDatabase
router *someRouter
email EmailSender
}
I have a single file inside every component called routes.go
where all the routing can live:
package app
func (s *server) routes() {
s.router.HandleFunc("/api/", s.handleAPI())
s.router.HandleFunc("/about", s.handleAbout())
s.router.HandleFunc("/", s.handleIndex())
}
This is handy because most code maintenance starts with a URL and an error report — so one glance at routes.go
will direct us where to look.
My HTTP handlers hang off the server:
func (s *server) handleSomething() http.HandlerFunc { ... }
Handlers can access the dependencies via the s server variable.
My handler functions don’t actually handle the requests, they return a function that does.
This gives us a closure environment in which our handler can operate:
func (s *server) handleSomething() http.HandlerFunc {
thing := prepareThing()
return func(w http.ResponseWriter, r *http.Request) {
// use thing
}
}
The prepareThing is called only once, so you can use it to do one-time per-handler initialisation, and then use the thing in the handler.
Be sure to only read the shared data, if handlers are modifying anything, remember you’ll need a mutex or something to protect it.
If a particular handler has a dependency, take it as an argument.
func (s *server) handleGreeting(format string) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
fmt.Fprintf(w, format, "World")
}
}
The format variable is accessible to the handlers.
I use http.HandlerFunc in almost every case now, rather than http.Handler.
func (s *server) handleSomething() http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
...
}
}
They are more or less interchangeable, so just pick whichever is simpler to read. For me, that’s http.HandlerFunc
.
Middleware functions take an http.HandlerFunc and return a new one that can run code before and/or after calling the original handler — or it can decide not to call the original handler at all.
func (s *server) adminOnly(h http.HandlerFunc) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
if !currentUser(r).IsAdmin {
http.NotFound(w, r)
return
}
h(w, r)
}
}
The logic inside the handler can optionally decide whether to call the original handler or not — in the example above, if IsAdmin is false, the handler will return an HTTP 404 Not Found and return (abort); notice that the h handler is not called.
If IsAdmin is true, execution is passed to the h handler that was passed in.
Usually I have middleware listed in the routes.go
file:
package app
func (s *server) routes() {
s.router.HandleFunc("/api/", s.handleAPI())
s.router.HandleFunc("/about", s.handleAbout())
s.router.HandleFunc("/", s.handleIndex())
s.router.HandleFunc("/admin", s.adminOnly(s.handleAdminIndex()))
}
If an endpoint has its own request and response types, usually they’re only useful for that particular handler.
If that’s the case, you can define them inside the function.
func (s *server) handleSomething() http.HandlerFunc {
type request struct {
Name string
}
type response struct {
Greeting string `json:"greeting"`
}
return func(w http.ResponseWriter, r *http.Request) {
...
}
}
This declutters your package space and allows you to name these kinds of types the same, instead of having to think up handler-specific versions.
In test code, you can just copy the type into your test function and do the same thing. Or…
If your request/response types are hidden inside the handler, you can just declare new types in your test code.
This is an opportunity to do a bit of storytelling to future generations who will need to understand your code.
For example, let’s say we have a Person
type in our code, and we reuse it on many endpoints. If we had a /greet
endpoint, we might only care about their name, so we can express this in test code:
func TestGreet(t *testing.T) {
is := is.New(t)
p := struct {
Name string `json:"name"`
}{
Name: "Mat Ryer",
}
var buf bytes.Buffer
err := json.NewEncoder(&buf).Encode(p)
is.NoErr(err) // json.NewEncoder
req, err := http.NewRequest(http.MethodPost, "/greet", &buf)
is.NoErr(err)
//... more test code here
It’s clear from this test, that the only field we care about is the Name
of the person.
If I have to do anything expensive when preparing the handler, I defer it until when that handler is first called.
This improves application startup time.
func (s *server) handleTemplate(files string...) http.HandlerFunc {
var (
init sync.Once
tpl *template.Template
tplerr error
)
return func(w http.ResponseWriter, r *http.Request) {
init.Do(func(){
tpl, tplerr = template.ParseFiles(files...)
})
if tplerr != nil {
http.Error(w, tplerr.Error(), http.StatusInternalServerError)
return
}
// use tpl
}
}
sync.Once ensures the code is only executed one time, and other calls (other people making the same request) will block until it’s finished.
Remember that doing this, you are moving the initialisation time from startup, to runtime (when the endpoint is first accessed). I use Google App Engine a lot, so this makes sense for me, but your case might be different so it’s worth thinking about where and when to use sync.Once in this way.
Our server type is very testable.
func TestHandleAbout(t *testing.T) {
is := is.New(t)
srv := server{
db: mockDatabase,
email: mockEmailSender,
}
srv.routes()
req := httptest.NewRequest("GET", "/about", nil)
w := httptest.NewRecorder()
srv.ServeHTTP(w, req)
is.Equal(w.StatusCode, http.StatusOK)
}
ServeHTTP
on the server, we are testing the entire stack including routing and middleware, etc. You can of course call the handler methods directly if you want to avoid thishttptest.NewRequest
and httptest.NewRecorder
to record what the handlers are doingis
testing mini-framework (a mini alternative to Testify) github.com/matryer/isIf you decide to implement some of these patterns in your own code, please share your progress with me on Twitter, and if you have other ideas that you’d like to discuss, please consider writing about them yourself.
A lot of our blog posts come out of the technical work behind a project we're working on called Pace.
We were frustrated by communication and project management tools that interrupt your flow and overly complicated workflows turn simple tasks, hard. So we decided to build Pace.
Pace is a new minimalist project management tool for tech teams. We promote asynchronous communication by default, while allowing for those times when you really need to chat.
We shift the way work is assigned by allowing only self-assignment, creating a more empowered team and protecting the attention and focus of devs.
We're currently live and would love you to try it and share your opinions on what project management tools should and shouldn't do.
What next? Start your 14 day free trial to see if Pace is right for your team
or you can share the URL directly:
https://pace.dev/blog/2018/05/09/how-I-write-http-services-after-eight-years.html
Thank you, we don't do ads so we rely on you to spread the word.
If you have any questions about our tech stack, working practices, or how project management will work in Pace, tweet us any time. :)
— pace.dev (@pacedotdev) February 25, 2020
We're excited to announce general availability of Pace #launch #preview #project-management
The tech stack at Pace #Tech #Golang #Svelte
Passive user preferences with persisted stores in Svelte #sveltejs #javascript #typescript