Service propagation from middleware chain to handlers

Hi,

how do you guys handle “service injection” concept in go webserver backends

  • do you propagate through middleware provider, by extending Request.Context so handlers can read it from there
  • share a global variable (enforce concurrency guards)
  • or do you make a Services trait which has a Tree of dependencies, its a type that probably implements http.Handler, has a chain of handlers to whom it injects to those that need certain services

What is your approach? currently I do this:

package views_provider

import (
	"context"
	"html/template"
	"net/http"
)

const ViewsKey = "VIEWS"

type ViewsProviderKey string

func ViewsProvider(root *template.Template) func(http.Handler) http.Handler {
	return func(next http.Handler) http.Handler {
		return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
			ctx := context.WithValue(r.Context(), ViewsProviderKey(ViewsKey), root)
			r = r.WithContext(ctx)

			next.ServeHTTP(w, r)
		})
	}
}

func Lookup() ViewsProviderKey {
	return ViewsProviderKey(ViewsKey)
}

use:

views, ok := req.Context().Value(views_provider.Lookup()).(*template.Template)

bootstrap server:

func (r *Router) Bootstrap() {
	// global middlewares
	r.Use(
		middleware.Recoverer,
		reqsize.RequestSize(1),
		views_provider.ViewsProvider(r.ViewsEngine),
		store_provider.SessionProvider(SESSION_STORE),
	)

	for _, registry := range RegistryList {
		registry(r)
	}
}

does that look discouraged or okay?

First off: might want to think about package names. From the docs:

Good package names are short and clear. They are lower case, with no under_scores or mixedCaps .

Second: I think you could argue this is an anti-pattern. From the docs:

Use context Values only for request-scoped data that transits processes and APIs

It seems like you’re trying to prevent a global, which is good. The pattern I’ve seen most is to create some sort of server or environment struct that stores your would-be global objects. So, something like this:

type Server struct {
    Views    *template.Template
    Sessions sessions.Store
    DB       *sql.DB
}

Then use a method receiver like so:

func (s *Server) handleIndex(w http.ResponseWriter, r *http.Request) {
	// You have a strongly-typed *template.Template with zero magic
	if err := s.Views.ExecuteTemplate(w, "index.html", nil); err != nil {
		http.Error(w, err.Error(), http.StatusInternalServerError)
	}
}

It looks like you’re using chi. So then in main or wherever you are bootstrapping your app, define your server:

func main() {
	// Set up our server with would-be globals
	srv := &Server{
		Views:    getViewsProvider(),
		Sessions: store_provider.SessionProvider(SESSION_STORE),
		DB:       getDB(),
	}
	// Middleware
	r := chi.NewRouter()
	r.Use(
		middleware.Recoverer,
		reqsize.RequestSize(1),
	)
	// Simple route example
	r.Get("/", srv.handleIndex)
	// Listen and serve!
	log.Fatal(http.ListenAndServe(":8080", r))
}

It’s fine to use context for values that are related that request. I stuff things like JWTs in the context all the time. Anyway, give that pattern a try!

2 Likes

Hi, I was using this pattern and still Am, but i was trying to decouple viewsprovider for whatever reason, but it seems logical to keep it global. thank you

If the idea is that, based on context, you want to use a different views provider, you could just add another layer of indirection. Where Views (or whatever you name it) has multiple providers and you switch based on context/HTTP header/whatever:

func (s *Server) handleIndex(w http.ResponseWriter, r *http.Request) {
	// Get the view provider
	vp := s.Views.Provider("this-comes-from-context")
	// You have a strongly-typed *template.Template with zero magic
	if err := vp.ExecuteTemplate(w, "index.html", nil); err != nil {
		http.Error(w, err.Error(), http.StatusInternalServerError)
	}
}
1 Like