Go: Dynamic web handler registration with echo

Go: Dynamic web handler registration with echo

At work, we use the Go Echo framework (https://echo.labstack.com/ ) for our API services. This HTTP framework is powerful and highly customizable. Sometimes, however, we need to register routes more dynamically at build time.

We have a few feature flags in our software that are supposed to disable parts of the APIs which are not yet stable or still under development. For this purpose, we use a pattern that leverages Go's special init() function. The init() function is executed before the main function in each module, and you can have multiple init() functions in one module.

For our use case, we have a generic handlers module that includes a utility file maintaining a list of InitFunctions, which are called after the main function initializes the handlers module by passing in references to the database interface and other required modules. The handlers/init.go file looks like this:

package handlers

import (
	"test/config"
	"test/db"

	"github.com/labstack/echo/v4"
)

type RegisterFunc func(g *echo.Group, con db.ConnectionI, conf *config.Config) error

var selfRegisterFuncs []RegisterFunc

func InitRoutes(g *echo.Group, con db.ConnectionI, conf *config.Config) error {
	for _, fn := range selfRegisterFuncs {
		if err := fn(g, con, conf); err != nil {
			return err
		}
	}
	return nil
}

Here, RegisterFunc is a type definition that should be implemented by functions inside the files for feature flag handlers. The InitRoutes function will be called by the main function after initializing the database connection and loading the config.

Individual handler implementations look like this:

package handlers

import (
	"fmt"
	"test/config"
	"test/db"

	"github.com/labstack/echo/v4"
)

func init() {
	selfRegisterFuncs = append(selfRegisterFuncs, registerRouteAHandler)
}

func registerRouteAHandler(g *echo.Group, con db.ConnectionI, conf *config.Config) error {
	fmt.Println("Registered Feature A")
	g.GET("/a", func(c echo.Context) error {
		return c.JSON(200, "Hello from Route A GET")
	})
	g.POST("/a", func(c echo.Context) error {
		return c.JSON(200, "Hello from Route A POST")
	})
	return nil
}

The init function is called before the main function and thus has time to register itself by appending its own RegisterFunc to the list of functions in selfRegisterFuncs. You can conditionally include individual handler functions at build time using Go build tags. For this, we add a special comment at the top of each file:

//go:build feature_routeb
// +build feature_routeb

package handlers

import (
	"fmt"
	"test/config"
	"test/db"

	"github.com/labstack/echo/v4"
)

func init() {
	selfRegisterFuncs = append(selfRegisterFuncs, registerRouteBHandler)
}

func registerRouteBHandler(g *echo.Group, con db.ConnectionI, conf *config.Config) error {
	fmt.Println("Registered Feature B")
	g.GET("/b", func(c echo.Context) error {
		return c.JSON(200, "Hello from Route B GET")
	})
	g.POST("/b", func(c echo.Context) error {
		return c.JSON(200, "Hello from Route B POST")
	})
	return nil
}

When building the API, you can use a list of Go tags to include specific features. For example:

go build -tags feature_routeb

This will result in RouteB being included:

Read more