/muxc

A golang http.ServeMux configurator

Primary LanguageGoMIT LicenseMIT

MUXC: A golang http.ServeMux configurator

Since 1.22 we can finally use variables when defining path patterns. For simple cases there is no need to use a 3rd party libraries like chi, echo or gorillamux for creating our servers.

But there is still not nice support for middleware chaining, and when defining several routes, it can be tedious to mantain our middleware and route stack.

Muxc is a tool that aims to simplify the process (inspired by sqlc), routes and middlewares are defined in a YAML file and running muxc command a routes.go file is generated that allow to configure your ServeMux.

Installation and usage

Installation: go install github.com/enolgor/muxc@latest

Usage: muxc -f <path-to-yaml-file>

By default if -f is not provided, it will look for a file name muxc.yaml in the same directory where the command is executed.

Once the routes.go file is created under your configured package directory, it can be used to configure your http.ServeMux, passing first the mux pointer and then any args specified in the yaml configuration file:

package main

import (
	"net/http"

	"github.com/enolgor/muxc/examples/basic/controllers"
	"github.com/enolgor/muxc/examples/basic/muxc"
)

func main() {
	mux := http.NewServeMux()
	muxc.ConfigureMux(mux, controllers.NewController())
	if err := http.ListenAndServe(":8080", mux); err != nil {
		panic(err)
	}
}

Full example is available under examples/basic directory.

Using docker

You can use muxc with docker, just run: docker run --rm -t -v $(pwd):/src -w /src muxc -f <path-to-yaml-file>. As mentioned above, you can omit passing -f ... if using muxc.yaml as your definition filename.

YAML Definition Syntax example

package: muxc #package name of generated routes
out: ./muxc #relative (to this file) directory to output generated routes file (typically should match the last part of the package)

imports: #any package that should be imported by the routes file, those can be used defining args, vars and middlewares
  - "github.com/enolgor/muxc/examples/basic/controllers"
  - "github.com/enolgor/muxc/examples/basic/handlers"
  - "github.com/enolgor/muxc/examples/basic/middlewares"

args: #arguments to pass to the mux configuration function
  ctrl: controllers.Controller

vars: #variables to declare inside the mux configuration function, should be used to declare aliases to long middleware definitions
  contentJson: Middleware(middlewares.SetHeader("Content-Type", "application/json"))
  acceptJson: Middleware(middlewares.SetHeader("Accept", "application/json"))

routes: #route groups array
  - use: #middlewares to apply to all paths of this route group, should be of type func(next http.HandlerFunc) http.HandlerFunc
    - Middleware(middlewares.RequestID) #Middleware() helper converts func(http.Handler) http.Handler to the HandlerFunc equivalent
    - Middleware(middlewares.Logger)
    base: /api/v1 #base path to prefix all paths of this route group
    paths: #semi-colon separated path/handler/middleware definition: <pattern> ; <handler>; <middlewares (comma separated, optional)>
      - GET /pet            ;handlers.ListPets(ctrl)     ;contentJson
      - GET /pet/{id}       ;handlers.ReadPet(ctrl)      ;contentJson
      - PUT /pet            ;handlers.CreatePet(ctrl)    ;acceptJson, contentJson
      - POST /pet           ;handlers.UpdatePet(ctrl)    ;contentJson
      - DELETE /pet         ;handlers.DeletePet(ctrl)

# middlewares are applied ordered in terms of how close they are to the handler, in the PUT path of this example that will be:
# - 1st. RequestID
# - 2nd. Logger
# - 3rd. contentJson
# - 4th. acceptJson
# finally the handler will be called

The following routes.go file will be generated:

// Code generated by muxc. DO NOT EDIT.
// versions:
//   muxc v1.0.0
// source: muxc.yaml

package muxc

import (
	"net/http"

	"github.com/enolgor/muxc/examples/basic/controllers"
	"github.com/enolgor/muxc/examples/basic/handlers"
	"github.com/enolgor/muxc/examples/basic/middlewares"
)

func chain(f http.HandlerFunc, middlewares ...func(http.HandlerFunc) http.HandlerFunc) http.HandlerFunc {
	for _, m := range middlewares {
		f = m(f)
	}
	return f
}

func Middleware(m func(http.Handler) http.Handler) func(http.HandlerFunc) http.HandlerFunc {
	return func(next http.HandlerFunc) http.HandlerFunc {
		return m(next).ServeHTTP
	}
}

func ConfigureMux(mux *http.ServeMux, ctrl controllers.Controller) {
	acceptJson := Middleware(middlewares.SetHeader("Accept", "application/json"))
	contentJson := Middleware(middlewares.SetHeader("Content-Type", "application/json"))
	mux.Handle("GET /api/v1/pet", chain(
		handlers.ListPets(ctrl),
		contentJson,
		Middleware(middlewares.Logger),
		Middleware(middlewares.RequestID),
	))
	mux.Handle("GET /api/v1/pet/{id}", chain(
		handlers.ReadPet(ctrl),
		contentJson,
		Middleware(middlewares.Logger),
		Middleware(middlewares.RequestID),
	))
	mux.Handle("PUT /api/v1/pet", chain(
		handlers.CreatePet(ctrl),
		contentJson,
		acceptJson,
		Middleware(middlewares.Logger),
		Middleware(middlewares.RequestID),
	))
	mux.Handle("POST /api/v1/pet", chain(
		handlers.UpdatePet(ctrl),
		contentJson,
		Middleware(middlewares.Logger),
		Middleware(middlewares.RequestID),
	))
	mux.Handle("DELETE /api/v1/pet", chain(
		handlers.DeletePet(ctrl),
		Middleware(middlewares.Logger),
		Middleware(middlewares.RequestID),
	))
}