Showcasing higher order functions with HTTP router
Tags: go, functional, webdev, tutorial
Published: 2022-05-23 16:00:56

The other day I was talking to a friend who was deep in the woods reworking a piece of software. There was a global `map` involved, where the keys are HTTP routes and values are functions. Plugins would add their routes to the map by assigning a function reference. Then a core setup function would register all routes from the map with the actual HTTP router. The purpose for the `map` wasn't quite clear to me and the friend told me some plugins conflict with each other (e.g. login with Google vs login with Microsoft). A core setup function would decide which of the conflicting plugins actually to use.

Maybe there's an alternative to the global `map`. Just let the plugins add their routes directly to the router! But what about the setup function discriminating on arbitrary conditions? Let the plugins decide for themselves! Enter higher order functions, which are defined as functions taking function type arguments or returning function types.

## Registering routes without direct router dependency

To add routes, assume we have a nifty library providing a router `struct`.

```go
func (a *NiftyRouter) Register(method, path string, handler func())
```

On the other hand there's a bunch of plugin functions eager to add routes. To avoid strongly coupling plugins and the concrete router `struct`, let's define a function type that expresses the intent of registering a route:

```go
type RegisterRouteFunc func(method, path string, handler func())
```

An implementation using the concrete router `struct` is a higher order function returning a function matching `RegisterRouteFunc` signature:

```go
func RegisterRouteNifty(router *NiftyRouter) RegisterRouteFunc {
    return func(method, path string, handler func()) {
        router.Register(method, path, handler)
    }
}
```

Notice how the `router` argument is used in the returned function, creating a [closure][0]. The performance penalty of closure creation is negligible because `RegisterRouteNifty` is called only once early in program runtime (more on that later).

## Defining the plugin interface

In the same vein, create a function type for the intent of adding a plugin. The function receives an argument of `RegisterRouteFunc`, conveying the fact that plugins can register HTTP routes.

```go
type AddPluginFunc func(register RegisterRouteFunc)
```

Each plugin provides an implementation of `AddPluginFunc`:

```go
func AddProfilePagePlugin() AddPluginFunc {
    return func(register RegisterRouteFunc) {
        register("GET", "/profile", func() {
            // ...
        })
    }
}
```

If there's some need for arbitrary conditions, add the required inputs as arguments to the named function. This creates another closure, but again the impact is negligible.

```go
func AddGoogleLoginPlugin(config *Configuration) AddPluginFunc {
    return func(register RegisterRouteFunc) {
        if (config.Foo) {
            register(/* ... */)
        }
    }
}
```

There might also be a `AddPluginFunc` function that composes several `AddPluginFunc`s into one function:

```go
func AddPlugins(plugins ...AddPluginFunc) AddPluginFunc {
    return func(register RegisterRouterFunc) {
        for _, f := range plugins {
            f(register)
        }
    }
}
```

## Putting it together

With the building blocks in place, the main function can be assembled:

```go
func main() {
    // Some requirements
    var config *Configuration
    var router *NiftyRouter

    register := RegisterRouteNifty(router)

    AddPlugins(
        AddGoogleLoginPlugin(config),
        AddMicrosoftLoginPlugin(config),
        AddProfilePagePlugin(),
        // ...
    )(register)

    // Start the router...
}
```

Of course, higher order functions can be used in many different circumstances. Some care has to be taken when creating closures to avoid performance penalties.

(The code examples were all written for this post.)

[0]: https://en.wikipedia.org/wiki/Closure_(computer_programming)