Skip to content

Creating routes

loveyourstack edited this page Apr 18, 2024 · 3 revisions

This page suggests how to structure your HTTP server routes to make use of generic CRUD functions with database store packages and save yourself a ton of boilerplate. Routing is done using gorilla/mux.

Application struct

Start with an application struct to hold properties which are common to the HTTP server app, the CLI app, and all other apps, such as logging and database connection pool. This is the example from the Northwind sample application:

type Application struct {
  Config   *nw.Config
  InfoLog  *slog.Logger
  ErrorLog *slog.Logger
  Db       *pgxpool.Pool
  Validate *validator.Validate
}

HTTP server application struct

Now make a struct specifically for the HTTP server, to which your routes will be attached. The Application struct above is embedded:

type httpServerApplication struct {
  *cmd.Application
  GetOptions  lys.GetOptions
  PostOptions lys.PostOptions
}

Schema routes

Make a method on the httpServerApplication struct for each database schema. This contains all routes for that schema. For example, for Northwind, the abridged version of the method for the "core" schema is:

func (srvApp *httpServerApplication) coreRoutes(apiEnv lys.Env) lys.RouteAdderFunc {

  return func(r *mux.Router) *mux.Router {

    endpoint := "/categories"

    categoryStore := corecategory.Store{Db: srvApp.Db}
    r.HandleFunc(endpoint, lys.Get[corecategory.Model](apiEnv, categoryStore)).Methods("GET")
    r.HandleFunc(endpoint+"/{id}", lys.GetById[corecategory.Model](apiEnv, categoryStore)).Methods("GET")
    r.HandleFunc(endpoint, lys.Post[corecategory.Input, corecategory.Model](apiEnv, categoryStore)).Methods("POST")
    r.HandleFunc(endpoint+"/{id}", lys.Put[corecategory.Input](apiEnv, categoryStore)).Methods("PUT")
    r.HandleFunc(endpoint+"/{id}", lys.Patch(apiEnv, categoryStore)).Methods("PATCH")
    r.HandleFunc(endpoint+"/{id}", lys.Delete(apiEnv, categoryStore)).Methods("DELETE")

    return r
  }
}

Subroutes function

Make a method on the httpServerApplication struct which defines the URL prefix and the method for each schema. For example, if there are methods for "common" and "core" schemas:

func (srvApp *httpServerApplication) getSubRoutes(apiEnv lys.Env) (subRoutes []lys.SubRoute) {

	subRoutes = append(subRoutes, lys.SubRoute{Url: "/common", RouteAdder: srvApp.commonRoutes(apiEnv)})
	subRoutes = append(subRoutes, lys.SubRoute{Url: "/core", RouteAdder: srvApp.coreRoutes(apiEnv)})

	return subRoutes
}

Main router function

Make a method on the httpServerApplication struct which returns the main application router, something like this:

func (srvApp *httpServerApplication) getRouter() http.Handler {

  // define Env struct needed for generic route handlers
  apiEnv := lys.Env{
    ErrorLog:    srvApp.ErrorLog,
    Validate:    srvApp.Validate,
    GetOptions:  srvApp.GetOptions,
    PostOptions: srvApp.PostOptions,
  }

  r := mux.NewRouter()

  // public routes
  r.HandleFunc("/", lys.Message("Welcome to the "+srvApp.Config.General.AppName+" API. Please log in.")).Methods("GET")

  // put all routes requiring auth behind "/a" for authed
  authedR := r.PathPrefix("/a").Subrouter()

  // add subroutes into main router
  for _, subRoute := range srvApp.getSubRoutes(apiEnv) {
    subRouter := authedR.PathPrefix(subRoute.Url).Subrouter()
    _ = subRoute.RouteAdder(subRouter)
  }

  return r
}

Since we are putting routes requiring authentication behind the prefix "/a", and using a further prefix per schema, the final URL for the core.category table endpoint is: "host:port/a/core/categories".

Use in main function

In our HTTP server main function, we instantiate Application, httpServerApplication, and attach the main route function to a new http.Server instance:

func main() {

  // create non-specific app
  app := &cmd.Application{
    Config:   &conf,
    InfoLog:  slog.New(slog.NewTextHandler(os.Stdout, nil)),
    ErrorLog: slog.New(slog.NewTextHandler(os.Stderr, nil)),
    Validate: validator.New(validator.WithRequiredStructEnabled()),
  }

  // create http server app
  srvApp := &httpServerApplication{
    Application: app,
    GetOptions:  lys.FillGetOptions(lys.GetOptions{}),   // use defaults
    PostOptions: lys.FillPostOptions(lys.PostOptions{}), // use defaults
  }

  // create HTTP server using srvApp's routes and handlers
  srv := &http.Server{
    Addr:    ":" + srvApp.Config.API.Port,
    Handler: srvApp.getRouter(),
  }

  // start server..
}

Clone this wiki locally