Dependency Inversion in Go HTTP Handlers

This post has a lot of Go, but I suspect its relevant to any programming language with first class functions.

I’ve been doing some more work in Go the last month or so. One bit of advice I really like and have been following from how I write HTTP service in Go after 13 years is this idea that one can use a factory (maker if you prefer to avoid OOP terminology) function to return a HTTP handler. Pass all the dependencies into the factory so dependencies are closed over and available to the handler.

It’s a really nice pattern and nice way to do inversion of control (IoC) without needing to do things that look closer to objects with constructors and dependencies. It’s still IoC though, just a nicer version than having a huge API struct or something with handler functions hanging off of it.

One thing I struggle with though is just how small to make these dependencies. In particular with things related to storing entities in some database. For example, a TodoRepository:

type Todo struct {
  Title string
  Description string
  Completed bool
}

type TodoRepository interface {
  GetByID(ctx context.Context, id string) (Todo, error)
  FindAll(ctx context.Context) ([]Todo, error)
  // other ways to query and store stuff, etc
}

All of these functions belong together. They will change together (high cohesion). They are likely coupled to some storage backend and thus some sort of shared resource (in memory, a database connection pool, etc). An interface and probably an implementation backed by a struct is the right tool for this job.

But now take an http handler to list all todos:

func ListAllTodos(repo TodoRepository) http.HandlerFunc {
  return function (w http.ResponseWriter, r *http.Request) {
    todos, err := repo.FindAll(r.Context())
    // error handling, serialization, etc
  }
}

Really this handler only cares about FindAll, but it receives all the baggage of the entire TodoRepository. Depending on the size of that interface, that might make testing complicated. Or require some sort of tooling to generate and work with test doubles. Or maintain a bespoke, in-memory version of TodoRepository just for tests.

One option is to reduce the surface area to a single function.

type findAllTods func (context.Context) ([]Todo, error)

func ListAllTodos(findAll findAllTodos) http.HandlerFunc {
  return function (w http.ResponseWriter, r *http.Request) {
    todos, err := findAll(r.Context())
    // error handling, serialization, etc
  }
}

Depending on the system, this might make the TodoRepository interface completely obsolete. It does not matter if a type as a whole implements any interface as long as there exists a function that adheres to the type.

And the code that glues together the API doesn’t change much either.

mux.Get("/todos", ListAllTodos(todoRepo))
// vs
mux.Get("/todos", ListAllTodos(todoRepo.FindAll))

Unit testing? Just use anonymous functions for the use case. No mocking or bespoke test double implementations required.

func TestListTodos_HandlesErrors(t *testing.T) {
  w := httptest.NewRecorder()
  r := httptest.NewRequest("GET", "/todos", nil)
  handle := ListAllTodos(function (ctx context.Context) ([]Todos, error) {
    return nil, errors.New("oh noz")
  })

  handle(w, r)

  if w.Code != http.StatusInternalServerError {
    t.Errorf("expected errors from finding todos to cause %d responses, got %d", http.StatusInternalServerError, w.Code)
  }
}

I’ve done both styles of factory/make functions in the past few months. I enjoy how easy it is to thoroughly unit test the narrow dependencies. On the flip side, I suspect there’s a lot more bang for the buck in integration and end-to-end testing APIs using something like huma’s testing utilities and interacting with real storage backends. Using some transactions to start with a clean slate each time. In those cases, it doesn’t matter how complicated a repo object is to set up for tests — the whole app is the system under test and whatever logic exists to set up the app is re-used in tests.

Posted in Go